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 & path | Handler | Summary |
|---|---|---|
POST /agents/{id}/documents/upload | upload_document | Ingest a UTF-8 .txt file (async — awaits the file read) |
GET /agents/{id}/documents | get_agent_documents | List documents, newest first |
GET /agents/{id}/chunks | get_agent_chunks | All stored chunks, oldest first |
POST /agents/{id}/documents/search | search_documents | Raw vector search (requires document_search tool) |
POST /agents/{id}/documents/ask | ask_agent_documents | Corrective-RAG question answering (requires document_search tool) |
Full request/response contracts: API Reference: Documents.
Upload pipeline
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:
- A
Documentrow is created withstatus="indexed"andchunks_count, committed, and refreshed (so itsidexists for chunk IDs). - For each chunk: a
DocumentChunkrow is added and the chunk is embedded and written to Chroma withvector_id = "agent_{aid}_document_{did}_chunk_{i}"and metadata{agent_id, document_id, filename, chunk_index}. - A final commit persists all chunk rows.
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/search | documents/ask | |
|---|---|---|
| What it does | Embeds the query, returns top-k scored chunks | Runs the LangGraph corrective-RAG graph |
| LLM calls | 1 (embedding only) | 3 (embedding + evaluator + answer) — 2 when refusing |
| Returns | raw chunk_text + score (Chroma distance — lower is better) | grounded answer, sources, retrieval_evaluation |
| ToolCall name | document_search | langgraph_corrective_rag |
| Use case | debugging retrieval, building custom UX | end-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
| Condition | Response |
|---|---|
| Agent missing | 404 |
Non-.txt, non-UTF-8, or empty file | 400 with a specific message |
document_search tool disabled | 403 |
| Embedding/LLM/Chroma failure in search/ask | 500; failed ToolCall committed first |
| Embedding failure during upload | 500; 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_typecolumn 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.
Related files
- Service: Documents — chunking/embedding/Chroma internals
- Service: RAG Graph — the ask pipeline
- API Reference: Documents