Developer Guide
This guide covers the technical implementation of Knowledge Assist, including data models, the retrieval tool, and API reference.
Architecture Overview
Knowledge Assist uses a hybrid search approach combining text and semantic search:
Data Model
KnowledgeBase
Stores knowledge base configuration and content sources.
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | Auto | Primary key |
code | string | Yes | Unique identifier (max 32 chars, lowercase alphanumeric + dashes) |
name | string | Yes | Display name (max 200 chars) |
urls | array[URL] | No | List of URLs to crawl |
files | array[URL] | No | List of file URLs to process |
description | text | No | Description of the knowledge base contents |
created_at | datetime | Auto | Creation timestamp |
updated_at | datetime | Auto | Last update timestamp |
Django Model Definition:
class KnowledgeBase(TimestampedModel):
code = models.CharField(max_length=32, unique=True, validators=[KnowledgeBaseCodeValidator()])
name = models.CharField(max_length=200)
urls = ArrayField(models.URLField(max_length=255), null=True, blank=True, default=list)
files = ArrayField(models.URLField(max_length=512), null=True, blank=True, default=list)
description = models.TextField(blank=True, null=True)
Document
Stores individual documents indexed from content sources.
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | Auto | Primary key |
knowledge_base | ForeignKey | Yes | Reference to KnowledgeBase |
external_id | string | Yes | Unique identifier from source (max 512 chars) |
title | string | No | Document title (max 255 chars) |
description | text | No | Document description |
body | text | No | Document content |
url | string | Yes | Source URL (max 2048 chars) |
last_crawled_at | datetime | Yes | When the document was last crawled |
created_at | datetime | Auto | Creation timestamp |
updated_at | datetime | Auto | Last update timestamp |
Django Model Definition:
class Document(TimestampedModel):
knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE)
external_id = models.CharField(max_length=512, unique=True)
title = models.CharField(max_length=255, blank=True)
description = models.TextField(blank=True, null=True)
body = models.TextField(blank=True, null=True)
url = models.CharField(max_length=2048)
last_crawled_at = models.DateTimeField()
CrawlJob
Tracks web crawling jobs.
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | Auto | Primary key |
knowledge_base | ForeignKey | Yes | Reference to KnowledgeBase |
state | string | Yes | Job state (created, queued, started, completed, failed) |
by | ForeignKey | Yes | User who initiated the crawl |
started_at | datetime | No | When crawling started |
ended_at | datetime | No | When crawling ended |
urls | array[URL] | Yes | URLs to crawl |
file_proxy_urls | array[URL] | Yes | File URLs to process |
created_at | datetime | Auto | Creation timestamp |
updated_at | datetime | Auto | Last update timestamp |
Crawl Job States:
class CrawlJobState(models.TextChoices):
CREATED = "created", "Created"
QUEUED = "queued", "Queued"
STARTED = "started", "Started"
COMPLETED = "completed", "Completed"
FAILED = "failed", "Failed"
RetrieveKnowledgeTool
The RetrieveKnowledgeTool is an LLM tool that retrieves relevant documents from a knowledge base.
Tool Schema
{
"name": "retrieve_knowledge",
"description": "Retrieve knowledge from the knowledge base",
"parameters": {
"type": "object",
"properties": {
"knowledge_base_code": {
"type": "string",
"description": "The code of the knowledge base to retrieve knowledge from."
},
"query": {
"type": "string",
"description": "The query based on which the knowledge will be retrieved."
},
"skip_evaluation": {
"type": "boolean",
"description": "Whether to skip evaluation after the tool is executed."
}
},
"required": ["query"]
}
}
Search Implementation
The tool performs hybrid search using two methods:
Text Search (BM25)
text_query_dict = {
"bool": {
"must": [{"match": {"text": {"query": query}}}],
}
}
text_search_response = await elasticsearch_client.search(
index=index_name,
query=text_query_dict,
size=max_num_results,
)
Semantic Search (Vector)
embedding_response = await openai_client.embeddings.create(
input=query,
model=settings.OPENAI_EMBEDDING_MODEL,
)
query_vector = embedding_response.data[0].embedding
knn_query_dict = {
"field": "vector",
"query_vector": query_vector,
"num_candidates": max_num_results,
"k": max_num_results,
}
vector_search_response = await elasticsearch_client.search(
index=index_name,
knn=knn_query_dict,
size=max_num_results,
)
Reciprocal Rank Fusion
Results from both search methods are combined using Reciprocal Rank Fusion (RRF):
def _reciprocal_rank_fusion(
self, rankings: list[list[KnowledgeDocument]], constant: int = 60
) -> list[KnowledgeDocument]:
rrf_scores: dict[KnowledgeDocument, float] = defaultdict(float)
for ranking in rankings:
for rank, result in enumerate(ranking, start=1):
rrf_scores[result] += 1 / (rank + constant)
ranked_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
return [document for document, _ in ranked_docs]
The constant (default 60) balances the influence of rank position across different ranking lists.
Context Length Limiting
Results are automatically limited to fit within the model's context window:
def _limit_results_to_context_length(self, documents: list[KnowledgeDocument]) -> list[KnowledgeDocument]:
num_messages_tokens = ONE_TIME_TOKEN_OVERHEAD + sum(
len(self.token_encoder.encode(str(message["content"]))) + PER_MESSAGE_TOKEN_OVERHEAD
for message in self.assistant.thread.messages
)
document_sizes = np.array([
len(self.token_encoder.encode(document.text)) + self.num_tokens_separators
for document in documents
])
k = document_sizes.cumsum().searchsorted(
self._get_max_total_tokens() - num_messages_tokens - MAX_OUTPUT_SIZE
)
return documents[:k]
Model Token Limits:
| Model | Max Tokens |
|---|---|
| gpt-5, gpt-5-mini, gpt-5-nano | 272,000 |
| gpt-4o, gpt-4o-mini | 128,000 |
| gpt-4-32k | 32,768 |
| gpt-4 | 8,192 |
| gpt-35-turbo-16k | 16,384 |
| gpt-35-turbo | 4,096 |
Response Format
The tool returns a RetrieveKnowledgeToolResponse:
class KnowledgeDocument(BaseModel):
title: str
source_url: str
text: str
class RetrieveKnowledgeToolResponse(ToolResponse):
documents: list[KnowledgeDocument]
skip_evaluation: bool
Automatic Tool Injection
When an Assistant has a knowledge_base_code configured, the system automatically:
- Adds the
retrieve_knowledgetool to the Assistant's available tools - Injects knowledge base instructions into the system prompt
Injected Instructions:
def get_knowledge_base_instructions(knowledge_base_code: str) -> str:
return f"""
You are a helpful office assistant.
Your task is to answer user questions by first calling the retrieve_knowledge tool...
You MUST always use the retrieve_knowledge tool to get the context by these parameters:
- query: {{Rephrase the question from the user to contain the essence of the question}}
- assistant_code: {knowledge_base_code}
"""
Elasticsearch Index Structure
Documents are stored in Elasticsearch with the following index naming convention:
{ACCOUNT_CODE}-{knowledge_base_code}-vectors-ann
Each document in the index contains:
| Field | Type | Description |
|---|---|---|
text | text | Document content for text search |
vector | dense_vector | Embedding vector for semantic search |
metadata.title | keyword | Document title |
metadata.source_url | keyword | Source URL |
API Reference
Knowledge Bases
List Knowledge Bases
GET /api/v1/knowledge-assist/knowledge-bases/
Create Knowledge Base
POST /api/v1/knowledge-assist/knowledge-bases/
Request Body:
{
"code": "support-articles",
"name": "Support Articles",
"urls": [
"https://help.example.com/articles"
],
"files": []
}
Get Knowledge Base
GET /api/v1/knowledge-assist/knowledge-bases/{code}/
Update Knowledge Base
PUT /api/v1/knowledge-assist/knowledge-bases/{code}/
Delete Knowledge Base
DELETE /api/v1/knowledge-assist/knowledge-bases/{code}/
Deleting a knowledge base also deletes the associated Elasticsearch index and all documents.
Crawl Jobs
List Crawl Jobs
GET /api/v1/knowledge-assist/knowledge-bases/{code}/crawl-jobs/
Start Crawl Job
POST /api/v1/knowledge-assist/knowledge-bases/{code}/crawl-jobs/
Response:
{
"queued": true,
"job_id": 123
}
Returns 409 Conflict if a crawl is already in progress.
Get Crawl Job
GET /api/v1/knowledge-assist/crawl-jobs/{id}/
Update Crawl Job State
PUT /api/v1/knowledge-assist/crawl-jobs/{id}/
Used by the crawler service to update job state.
Documents
List Documents
GET /api/v1/knowledge-assist/knowledge-bases/{code}/documents/
Query Parameters:
| Parameter | Description |
|---|---|
external_id | Filter by external ID |
Create Document
POST /api/v1/knowledge-assist/knowledge-bases/{code}/documents/
Request Body:
{
"external_id": "doc-123",
"title": "Getting Started Guide",
"description": "Introduction to our product",
"body": "Full document content...",
"url": "https://help.example.com/getting-started",
"last_crawled_at": "2024-01-15T10:30:00Z"
}
Batch Update Documents
POST /api/v1/knowledge-assist/knowledge-bases/{code}/documents/batch/
Request Body:
[
{
"external_id": "doc-123",
"title": "Updated Title",
"body": "Updated content..."
},
{
"external_id": "doc-456",
"title": "Another Document",
"body": "Content..."
}
]
Update Document
PUT /api/v1/knowledge-assist/knowledge-bases/{code}/documents/{id}/
Delete Document
DELETE /api/v1/knowledge-assist/knowledge-bases/{code}/documents/{id}/
Background Tasks
crawl_knowledge_base_urls
Initiates a crawl request to the Knowledge Search service:
@app.task(autoretry_for=(requests.RequestException, ConnectionError), retry_backoff=True)
def crawl_knowledge_base_urls(crawl_job_id: int) -> None:
url = f"{settings.KNOWLEDGE_SEARCH_API_URL}/api/assistants/{knowledge_base.code}/crawl-requests"
response = requests.post(
url=url,
json={
"crawl_job_id": str(job.id),
"urls": job.urls,
"file_proxy_urls": job.file_proxy_urls,
},
)
update_knowledge_base_document
Syncs a document to the Knowledge Search service:
@app.task(autoretry_for=(ConnectionError,), retry_backoff=True)
def update_knowledge_base_document(document_id: int) -> None:
url = f"{settings.KNOWLEDGE_SEARCH_API_URL}/api/assistants/{knowledge_base.code}/documents"
payload = {
"id": document.id,
"title": document.title,
"description": document.description,
"body": document.body,
"url": document.url,
"last_crawled_at": document.last_crawled_at.strftime("%Y-%m-%d %H:%M:%S"),
}
response = requests.post(url=url, json=payload)
delete_elastic_index
Deletes an Elasticsearch index:
@app.task()
def delete_elastic_index(index_name: str) -> None:
elastic = Elasticsearch(settings.ELASTIC_URL, ...)
elastic.indices.delete(index=index_name, ignore_unavailable=True)
Source Code References
| Component | Location |
|---|---|
| KnowledgeBase model | knowledge_assist/models.py:61-83 |
| Document model | knowledge_assist/models.py:144-157 |
| CrawlJob model | knowledge_assist/models.py:114-141 |
| RetrieveKnowledgeTool | assistants/tools/retrieve_knowledge.py |
| Knowledge base instructions | backend/domains/assistants/models/instructions.py:10-29 |
| KnowledgeBaseViewSet | knowledge_assist/views.py:75-86 |
| DocumentViewSet | knowledge_assist/views.py:148-209 |
| Background tasks | knowledge_assist/tasks.py |
Related Documentation
- Overview: Conceptual overview
- User Guide: Configuration instructions
- Assistants Developer Guide: Assistant evaluation flow