Skip to content
4 min read · 748 words

Routers — documents.py

Five endpoints that turn .txt files into a per-agent knowledge base and answer questions over it — the HTTP face of the RAG pipeline.

Purpose

backend/app/routers/documents.py (~290 lines) handles the document lifecycle: ingest (upload → chunk → embed → index), inspect (documents, chunks), and query (raw vector search, corrective-RAG ask). Like the agents router it is mounted with prefix="/agents", so all routes nest under /agents/{agent_id}/....

Endpoint inventory

Method & pathHandlerSummary
POST /agents/{id}/documents/uploadupload_documentIngest a UTF-8 .txt file (async — awaits the file read)
GET /agents/{id}/documentsget_agent_documentsList documents, newest first
GET /agents/{id}/chunksget_agent_chunksAll stored chunks, oldest first
POST /agents/{id}/documents/searchsearch_documentsRaw vector search (requires document_search tool)
POST /agents/{id}/documents/askask_agent_documentsCorrective-RAG question answering (requires document_search tool)

Full request/response contracts: API Reference: Documents.

Upload pipeline

python
if not file.filename.endswith(".txt"):
    raise HTTPException(400, "Only .txt files are supported in this step")
try:
    text = file_bytes.decode("utf-8")
except UnicodeDecodeError:
    raise HTTPException(400, "File must be UTF-8 encoded text")
chunks = split_text_into_chunks(text)
if not chunks:
    raise HTTPException(400, "File is empty or could not be processed")

Then, per upload:

  1. A Document row is created with status="indexed" and chunks_count, committed, and refreshed (so its id exists for chunk IDs).
  2. For each chunk: a DocumentChunk row is added and the chunk is embedded and written to Chroma with vector_id = "agent_{aid}_document_{did}_chunk_{i}" and metadata {agent_id, document_id, filename, chunk_index}.
  3. A final commit persists all chunk rows.
Rendering diagram…

Embedding happens inside the chunk loop after the Document row is committed. If OpenAI fails mid-file, the request 500s with the document row already persisted (status="indexed", full chunks_count) but only some vectors stored, and pending chunk rows rolled back. Re-uploading is the practical recovery. Flagged in Known Issues.

Search vs. Ask

Both require the document_search tool (403 otherwise) and log a ToolCall (success or failure) — but they answer different questions:

documents/searchdocuments/ask
What it doesEmbeds the query, returns top-k scored chunksRuns the LangGraph corrective-RAG graph
LLM calls1 (embedding only)3 (embedding + evaluator + answer) — 2 when refusing
Returnsraw chunk_text + score (Chroma distance — lower is better)grounded answer, sources, retrieval_evaluation
ToolCall namedocument_searchlanggraph_corrective_rag
Use casedebugging retrieval, building custom UXend-user Q&A

Pipeline details: Retrieval and Corrective RAG.

Local helpers (duplicated by design-debt)

agent_has_tool() and log_tool_call() are copied from the agents router rather than shared — the documents-router copy of log_tool_call lacks the model_name/tokens/cost/agent_run_id parameters, so document tool calls carry no cost estimates. Extracting a shared helper module is an obvious refactor (Roadmap).

Also note the import quirk at the top of the file: generate_rag_answer is imported from openai_service and then re-imported from rag_graph_service (which itself re-exports it) — harmless shadowing, cleaned up whenever the file is next touched.

Error handling

ConditionResponse
Agent missing404
Non-.txt, non-UTF-8, or empty file400 with a specific message
document_search tool disabled403
Embedding/LLM/Chroma failure in search/ask500; failed ToolCall committed first
Embedding failure during upload500; see partial-failure note above (no ToolCall is logged for uploads)

Security notes

Uploads are unauthenticated and unbounded in size — a large file drives unbounded embedding spend. Tenant isolation relies on the agent_id metadata filter at query time. See Security.

Future improvements

  • PDF/DOCX ingestion (the file_type column already anticipates more types).
  • Batch embeddings for uploads; file-size limits.
  • Log an upload ToolCall (currently ingestion is invisible to the observability dashboard).
  • Document deletion endpoint that also removes Chroma vectors.