📚 Docs / Backend — FastAPI server, agents, tools, retrieval & scan

Backend — FastAPI server, agents, tools, retrieval & scan

This document covers everything Python-side: the HTTP API, the AI routers, tools, the retrieval pipeline, configuration, and the compliance scanner.

1. Server basics


2. HTTP API reference

Chat & files

MethodPathPurpose
GET/api/health{status, service} — used by the app's boot screen
POST/api/chatNon-streaming chat (collects events, returns final answer)
POST/api/chat/streamSSE streaming chat (primary path used by the UI)
POST/api/chat/resumeSSE resume after human-in-the-loop approval
POST/api/uploadSave an attached file into the active project folder (a workspace must be selected first — there is no default data/raw/ anymore), returns {path, filename, size}
POST/api/pipeline/configApply a pipeline-layout JSON (see PIPELINE_JSON.md) — reconfigures retrieval at runtime (rerank on/off, search mode, top_k, dense/sparse weights); optional query overrides top_k, top_k_retrieval, dense_weight, sparse_weight
GET/api/pipeline/configCurrent active runtime retrieval config
DELETE/api/pipeline/configReset retrieval to file defaults

Workspace (user-picked folder) — REQUIRED for chat/upload

MethodPathPurpose
POST/api/workspace{path} — make a folder the active workspace; uploads land directly in the folder, and the ChromaDB / BM25 / processed / history files all resolve under <folder>/workspace/; returns {active, path, name, workspace_dir, gpu_profile, files} (supported docs discovered in the folder)
GET/api/workspaceCurrent workspace ({active, path, name, workspace_dir, files})
DELETE/api/workspaceReset — no workspace active afterwards; chat/upload stay disabled until a new folder is picked
POST/api/workspace/ingestIngest every supported document in the workspace folder into its ChromaDB index; returns {chunks, path, elapsed_ms}
POST/api/workspace/ingest/streamSSE ingest with progress (ingest_start, ingest_progress, ingest_done, ingest_cancelled, ingest_error); ?force=1 re-indexes already-indexed files
POST/api/workspace/ingest/cancelStop an in-flight streaming ingest cleanly at the next file boundary
GET/api/workspace/historyPer-project saved state ({history}) from <project>/workspace/history.json
POST/api/workspace/historyPersist per-project state (chat history + working set + GPU profile)
GET/api/workspace/watch/statusBackground folder-watcher status (watching?, last scan, what it indexed)

A workspace is mandatory. Chat (/api/chat, /api/chat/stream, /api/chat/resume) and uploads error with "No workspace selected. Select a project folder first." when none is active — there is no default data/ folder fallback anymore. The folder is picked through a native Electron directory dialog (folder:select IPC) so the real absolute path reaches the backend.

Storage layout: with a workspace active, get_raw_dir() returns the project folder itself (uploads land directly in the user's folder, visible as project documents), while processed temp files, the ChromaDB index, the BM25 index and history.json all live under <project>/workspace/ (workspace/processed, workspace/chroma_db, workspace/bm25_index, workspace/history.json). The override is runtime-only (resets on restart / DELETE).

ChatRequest fields: query, mode (rag default | retrieval_only | direct), top_k (default 5), skip_rerank, temperature (default 0.3), conversation_id, history (prior [{role, content}] turns for direct mode), context_hint (raw selected passage from the native right-click menu — the pipeline retrieves the EXACT chunk the selection came from and cites it), and attachment fields attached_file_path(s) / attached_filename(s).

mode behavior:

GPU manager

MethodPathPurpose
GET/api/gpu/statusDevice, free/total VRAM, per-model placement + hotness, resident Ollama models
GET/api/gpu/profileThe currently-active residency profile
POST/api/gpu/profileApply a profile now and persist it to the active project's workspace/history.json

Playbook scan

MethodPathPurpose
GET/api/playbooksList available playbooks ({playbooks: [{playbook_id, name, path, rule_count}]})
POST/api/scanNon-streaming scan of an indexed document
POST/api/scan/streamSSE scan with progress (scan_start, status, chunk_classified, rule_check, flag_found, scan_done, error)
POST/api/scan/uploadSSE save file → ingest (chunk/embed/store) → scan
GET/api/flagsList flags, optional document_id / status filters
GET/api/flags/{flag_id}Single flag
POST/api/flags/{flag_id}/resolveaction: resolved
GET/api/documentsFile names currently indexed in ChromaDB

3. Chat flow (end-to-end)

User message + optional attachments
  └─ POST /api/chat/stream (SSE)
       └─ router_intent.run_intent_loop(query, llm, max_iterations, top_k,
                                         skip_rerank, conversation_id,
                                         attached_file_paths, attached_filenames,
                                         context_hint)
            ├─ intent classification event ("intent")
            ├─ planner → writes a 1-2 sentence plan, streams "plan" event,
            │   tool detected from plan
            ├─ executes tools (search_documents, verify_relevance,
            │   scan_document, ingest_file) via _impl functions
            ├─ streams: thinking / tool_call / tool_result / status / node_start
            │   / node_complete / sources / token
            └─ done event: {answer, sources, timing}

SSE event types the frontend consumes (see frontend/src/services/api.ts): meta, intent, status, thinking, tool_call, plan, tool_result, node_start, node_complete, sources, interrupted, resumed, error, token, done. (Workspace ingest has its own event set: ingest_start, ingest_progress, ingest_done, ingest_cancelled, ingest_error.)


4. Agents, tools & modes

A plan-based orchestration. Key nodes (async generators that yield events):

A LangGraph ReAct agent (ChatOllama + get_all_tools()), used by retrieval_only/direct modes and resume:

ToolSignatureWhat it does
search_documents(query, top_k=5, skip_rerank=False)Runs the retrieval pipeline over ChromaDB (+BM25) and returns ranked chunks with file/page/section/score
verify_relevance(query, documents)LLM check for entity/topic match; returns verified flags
scan_document(document_id, playbook_id="default")Runs the compliance scanner over an indexed document, persists flags
ingest_file(file_path)Parse → chunk → embed → store into ChromaDB; makes the file searchable/scannable

get_all_tools() / get_tools_for_mode(mode) build the LangChain-compatible tool list.


query
 ├─ embed (BGE-M3, 1024-dim, L2-normalized)
 ├─ dense search  — ChromaDB (<project>/workspace/chroma_db when a workspace
 │                  is active, otherwise data/chroma_db; collection "legal_chunks",
 │                  cosine/HNSW), top_k_retrieval=50
 ├─ sparse search — BM25 (<project>/workspace/bm25_index or data/bm25_index), top 50
 ├─ merge: hybrid modes (default "hybrid" merged with combine_and_dedup;
 │   rrf and boost_only configurable via SearchConfig.hybrid_merge_strategy)
 ├─ rerank — BGE-Reranker-v2-M3 cross-encoder, top_k_final=5, score >= 0.0
 └─ QueryResponse{query, results[{file_name, page, section, score, text}], latency_ms}

All storage paths resolve through the active workspace (see §2 Workspace): when a user-picked folder is active, VectorStore points at <project>/workspace/chroma_db and SparseRetriever at <project>/workspace/bm25_index, so search and ingestion are isolated per project. RetrievalPipeline.ingest_directory(path) skips the workspace's own storage dirs by default.

RetrievalPipeline.ingest_file(path)DocumentIngester (Docling parse + OCR) → StructureAwareChunker (480-token chunks, respect_sections=True) → embed → store → returns chunk count. Also builds/updates the BM25 index.

All tunables are dataclasses, overridable with PLR_* env vars:


A clause-level compliance checker:

document (indexed or just-uploaded)
 └─ scan_document_streaming(document_id, playbook, llm, skip_classification)
      ├─ _fetch_document_chunks → chunks with page/section metadata
      ├─ classify clause type per chunk (embedding classifier, BGE-M3 refs)  [can be skipped]
      ├─ for each rule in playbook: rule check (rule → clause type, severity, keywords/LLM)
      ├─ flag_found events: {flag_id, document_id, chunk_id, section_title,
      │    page_number, rule_id, rule_description, severity, clause_text, reason, confidence}
      └─ scan_done event with result{total_chunks, classified_chunks, total_flags, timing}


create_chat_pipeline()ChatPipeline.chat(query, mode, top_k) — retrieval → RAG prompt with context → LLM generation → formatted response with citations. Used by /api/chat for retrieval_only/direct modes.


An alternative node-graph implementation with RetrievalNode, VerificationNode, GenerationNode, PostGenCheckNode, AbstainNode, SuccessNode; build_rag_pipeline(), run_graph_pipeline(), and streaming variants (run_retrieval_only_streaming, run_direct_streaming, run_graph_pipeline_streaming).

Questions, answered

Short, self-contained answers about this guide.

How do I call the chat API?

POST /api/chat/stream returns an SSE stream of events — status updates, tool calls, retrieval results, reasoning, tokens, and a final done event carrying the answer and cited sources. The UI consumes this stream; a non-streaming /api/chat also exists.

How does routing work?

The intent router classifies each question and plans a route: SEARCH your documents, SCAN a document against the playbook, INGEST files, or VERIFY results. A legacy ReAct agent mode with human-in-the-loop approval is available for retrieval_only and direct flows.

What retrieval tools exist?

The backend exposes hybrid retrieval — BM25 sparse plus dense vectors fused and reranked — and parses tool output into the sources shown in the UI, including chunk scores and file references.