Skip to main content

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.

FieldTypeRequiredDescription
idintegerAutoPrimary key
codestringYesUnique identifier (max 32 chars, lowercase alphanumeric + dashes)
namestringYesDisplay name (max 200 chars)
urlsarray[URL]NoList of URLs to crawl
filesarray[URL]NoList of file URLs to process
descriptiontextNoDescription of the knowledge base contents
created_atdatetimeAutoCreation timestamp
updated_atdatetimeAutoLast 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.

FieldTypeRequiredDescription
idintegerAutoPrimary key
knowledge_baseForeignKeyYesReference to KnowledgeBase
external_idstringYesUnique identifier from source (max 512 chars)
titlestringNoDocument title (max 255 chars)
descriptiontextNoDocument description
bodytextNoDocument content
urlstringYesSource URL (max 2048 chars)
last_crawled_atdatetimeYesWhen the document was last crawled
created_atdatetimeAutoCreation timestamp
updated_atdatetimeAutoLast 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.

FieldTypeRequiredDescription
idintegerAutoPrimary key
knowledge_baseForeignKeyYesReference to KnowledgeBase
statestringYesJob state (created, queued, started, completed, failed)
byForeignKeyYesUser who initiated the crawl
started_atdatetimeNoWhen crawling started
ended_atdatetimeNoWhen crawling ended
urlsarray[URL]YesURLs to crawl
file_proxy_urlsarray[URL]YesFile URLs to process
created_atdatetimeAutoCreation timestamp
updated_atdatetimeAutoLast 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:

ModelMax Tokens
gpt-5, gpt-5-mini, gpt-5-nano272,000
gpt-4o, gpt-4o-mini128,000
gpt-4-32k32,768
gpt-48,192
gpt-35-turbo-16k16,384
gpt-35-turbo4,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:

  1. Adds the retrieve_knowledge tool to the Assistant's available tools
  2. 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:

FieldTypeDescription
texttextDocument content for text search
vectordense_vectorEmbedding vector for semantic search
metadata.titlekeywordDocument title
metadata.source_urlkeywordSource 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}/
warning

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:

ParameterDescription
external_idFilter 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

ComponentLocation
KnowledgeBase modelknowledge_assist/models.py:61-83
Document modelknowledge_assist/models.py:144-157
CrawlJob modelknowledge_assist/models.py:114-141
RetrieveKnowledgeToolassistants/tools/retrieve_knowledge.py
Knowledge base instructionsbackend/domains/assistants/models/instructions.py:10-29
KnowledgeBaseViewSetknowledge_assist/views.py:75-86
DocumentViewSetknowledge_assist/views.py:148-209
Background tasksknowledge_assist/tasks.py