Knowledge Bases & RAG
A knowledge base collection is a set of documents that Brutor chunks, embeds and stores in a per-tenant vector store (Qdrant, collection brutor_kb_{tenant_id}). Once a collection is bound to a resource group, every LLM call made by that group’s users is automatically grounded: the gateway retrieves the most relevant chunks and injects them as context before the request reaches the provider.
The developer-relevant consequence up front:
| Admin API base | http://localhost:5050/v1/admin/knowledge-base (note the hyphen) |
| Portal API base | http://localhost:8100/v1/portal/knowledgebases (plural, one word) |
| Auth | Admin JWT (admin API) · portal JWT (portal API) |
How a request gets grounded
Section titled “How a request gets grounded”When an LLM call arrives at the proxy for a group with KB bindings:
- The last user message becomes the retrieval query.
- The gateway runs hybrid search — dense vectors plus sparse BM25, fused with Reciprocal Rank Fusion — over the group’s collections, applying the collection’s
similarity_thresholdandtop_k. (An optional cross-encoder reranker can be enabled viaBRUTOR_RERANKER_URL.) - Chunks are accumulated until the
max_context_tokensbudget is hit, formatted as a numbered context block, and injected as a system message (after your system messages, before user messages) — using the collection’ssystem_prompt_templateif set ({context}placeholder). - The proxy log records
rag_chunks_retrievedfor the call.
When multiple collections are bound to a group, settings merge generously: union of collections, max top_k, min similarity_threshold, max max_context_tokens.
Set up a collection
Section titled “Set up a collection”-
Create the collection:
Terminal window curl -s -X POST http://localhost:5050/v1/admin/knowledge-base/collections \-H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \-d '{"name": "sales-playbooks", "description": "EMEA sales enablement","top_k": 5, "similarity_threshold": 0.75, "max_context_tokens": 2000,"group_ids": ["group-01ABC..."]}'{"id": "kbc-01K1...","name": "sales-playbooks","enabled": true,"top_k": 5,"similarity_threshold": 0.75,"max_context_tokens": 2000,"document_count": 0}Tunables:
top_k(1–50),similarity_threshold(0.0–1.0 cosine cutoff),max_context_tokens(100–32000),system_prompt_template(≤4096 chars,{context}placeholder). Rebind groups later withPOST .../collections/{id}/groups. -
Upload documents (
multipart/form-data; optional form fieldsname,version,author):Terminal window curl -s -X POST \http://localhost:5050/v1/admin/knowledge-base/collections/kbc-01K1.../documents \-H "Authorization: Bearer $ADMIN_JWT" \-F "file=@q3-playbook.pdf" -F "name=Q3 Playbook" -F "version=2026.3"import requestsr = requests.post(f"{BASE}/v1/admin/knowledge-base/collections/{coll_id}/documents",headers={"Authorization": f"Bearer {ADMIN_JWT}"},files={"file": open("q3-playbook.pdf", "rb")},data={"name": "Q3 Playbook", "version": "2026.3"},)doc = r.json() # doc["status"] == "pending"const form = new FormData();form.append("file", new Blob([await fs.readFile("q3-playbook.pdf")]), "q3-playbook.pdf");form.append("name", "Q3 Playbook");const res = await fetch(`${BASE}/v1/admin/knowledge-base/collections/${collId}/documents`, {method: "POST",headers: { Authorization: `Bearer ${ADMIN_JWT}` },body: form,});const doc = await res.json(); // doc.status === "pending"Supported types: PDF, DOCX/XLSX/PPTX (and legacy DOC/XLS/PPT), plain text, Markdown, HTML, CSV. Unsupported types →
400. -
Poll until ready. Upload is asynchronous — the
201response hasstatus: "pending". The KB uploader service (a separate worker) extracts text, chunks (500 tokens, 50 overlap, sentence-aware), embeds through the gateway, and upserts vectors. Poll the document untilstatusisready:Terminal window curl -s http://localhost:5050/v1/admin/knowledge-base/documents/kbd-01K1... \-H "Authorization: Bearer $ADMIN_JWT"{"id": "kbd-01K1...","name": "Q3 Playbook","status": "ready","chunk_count": 42,"embedding_model": "text-embedding-3-small"}Lifecycle:
pending → processing → ready | error(error_messagecarries the failure).POST .../documents/{id}/reembedre-queues a document after content or model changes. -
Preview retrieval with the admin search endpoint:
Terminal window curl -s -X POST \http://localhost:5050/v1/admin/knowledge-base/collections/kbc-01K1.../search \-H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \-d '{"query": "Q3 EMEA renewal terms", "top_k": 8}'{"query": "Q3 EMEA renewal terms","total": 3,"results": [{"chunk": {"document_id": "kbd-01K1...", "chunk_index": 7,"content": "Renewal terms for EMEA in Q3 ..."},"score": 0.87,"document_name": "Q3 Playbook"}]} -
Chat, grounded. Any LLM call or portal chat made by a member of the bound group is now grounded automatically — no request changes.
Listing KBs from a portal client
Section titled “Listing KBs from a portal client”Building your own chat client on the Portal API? List what the signed-in user can reach (access is enforced by the resource-group join):
curl -s "http://localhost:8100/v1/portal/knowledgebases/?enabled_only=true" \ -H "Authorization: Bearer $PORTAL_JWT"[ { "id": "kbc-01K1...", "name": "sales-playbooks", "enabled": true, "top_k": 5, "document_count": 12, "ready_count": 12 }]GET /v1/portal/knowledgebases/{collection_id} returns one collection, access-checked. There is deliberately no portal search endpoint — retrieval only happens inside governed LLM calls.
Embeddings: one vector space per tenant
Section titled “Embeddings: one vector space per tenant”The embedding model is tenant-wide (default_embedding_model_id on the tenant), not per-collection — guaranteeing a single consistent vector space. Any embedding-capable model in your catalog works (OpenAI, Ollama, vLLM, …), and all embedding traffic — ingest and query — routes through the gateway itself, so it’s metered, quota’d and policy-checked like everything else.
Changing the tenant’s embedding model triggers a tenant-wide re-embed (all documents flip back to pending). A dimension mismatch between existing vectors and the current model raises a kb_vector_dim_mismatch alert; the remedy is that re-embed.
Access control and isolation
Section titled “Access control and isolation”- Search scope = resource-group binding. A collection is retrievable only by members of its bound groups — enforced identically in the portal listing and the proxy’s retrieval path.
- Tenant isolation is cryptographic. Every Qdrant operation mints a fresh 300-second tenant-scoped JWT naming exactly
brutor_kb_{tenant_id}— cross-tenant access is rejected at the vector-store layer, per call. - Connector documents carry source ACLs. Content synced from external tools keeps its origin permissions (e.g. Slack channel membership, Drive sharing): at retrieval time a connector chunk is only eligible if the requesting user’s mapped principals intersect the document’s ACL. Direct uploads are governed by the group binding alone.
Syncing external sources
Section titled “Syncing external sources”The KB connector service pulls content on a per-connector cron schedule and feeds it through the same ingestion pipeline (source: "connector"). Eight connector types ship: Confluence, Notion, Google Drive, Slack, GitHub, Jira, SharePoint, and a web crawler. Configure them in the Admin UI (Knowledge Base → Connectors) or via /v1/admin/kb-connectors — catalog at GET /v1/admin/kb-connectors/catalog, per-connector POST .../{id}/sync for an immediate run, GET .../{id}/runs for history. Credentials are encrypted at rest and never returned by the API.
Gotchas
Section titled “Gotchas”- The admin base path is hyphenated (
knowledge-base); the portal path is one plural word (knowledgebases). - A
201upload response means queued, not searchable — the KB uploader worker must be running (it is part of the standard compose stack) and the document must reachready. - No request-level KB selection and no per-collection embedding model — both are deliberate design decisions (group-driven grounding; single tenant vector space).
Related
Section titled “Related”- Build a custom portal — the chat surface where grounding shows up
- Call LLMs through the gateway — grounded calls are ordinary LLM calls
- Resource groups — the binding that scopes retrieval
- Portal API reference — the full portal surface
