app.py
Application entrypoint
Creates the FastAPI app, configures CORS and structured error handlers, logs startup configuration, and mounts the static frontend at /app.
This project is a complete retrieval-augmented generation application: a FastAPI backend, a persistent local vector library, a grounded answer chain, and a plain HTML/CSS/JS interface. It is small enough to read end to end, and honest about what it does not do.
Most chatbot demos end at a prompt. The genuinely interesting engineering is earlier: deciding how a document becomes chunks, how those chunks become vectors, which passages are retrieved for a question, and how the answer proves where it came from. Those choices determine whether the system is useful or merely convincing.
This project treats RAG as a pipeline with inspectable stages. Ingestion, retrieval, generation and transport live in separate modules. The frontend is deliberately plain, the configuration is environment-driven, and the tests run against isolated temporary storage with fake models. The goal was a system that can be demonstrated, explained and debugged without hidden services.
The design stance is simple: answer from the provided context, show the passages, fail with a readable message, and never present an unverified capability as a feature. Chat generation uses an external API; embeddings and the vector library run locally.
Grounded does not mean guaranteed. The system is designed to make its evidence visible so a reader can check it.
Ingestion writes to the vector library. Question answering reads from it. Select any component to see what it owns.
1Client
2API and services
3Ingestion path
4Question-answering path
5Storage and configuration
The backend is grouped by responsibility rather than by layer alone, so each stage of the pipeline has one obvious home.
app.py
Creates the FastAPI app, configures CORS and structured error handlers, logs startup configuration, and mounts the static frontend at /app.
api/
Routes for chat, documents and health. Pydantic schemas validate requests and shape responses; api/errors.py builds the shared error envelope.
services/
document_service.py streams and validates uploads, lists the library and deletes documents. chat_service.py validates document selection, builds the retriever and runs the chain.
ingestion/
Format-specific loaders, the recursive character splitter, and the pipeline that computes the content hash and attaches doc_id, filename and chunk_index.
embeddings/
Loads sentence-transformers/all-MiniLM-L6-v2 once per process, on CPU, with normalized embeddings.
vectorstore/
Owns the Chroma collection: upserting chunks, pruning stale IDs, listing documents, counting chunks and deleting by doc_id.
retrieval/
Builds a similarity retriever with a configurable k and an optional doc_id filter for scoped questions.
chains/
The real prompt template and the LCEL chain that retrieves, formats context, calls the model, parses text and short-circuits empty retrieval.
generation/
Creates the cached Groq client and translates provider failures into typed exceptions the API can map to readable responses.
config/ + utils/
Environment-driven settings, filename sanitisation, document-ID validation, file-signature checks and logging setup.
These are the choices that shaped the implementation, with the reasoning that made them worth keeping.
Chat generation uses Groq's openai/gpt-oss-20b at temperature 0. Embeddings use a local sentence-transformers model. This keeps ingestion free of a second API key and per-chunk cost, while keeping generation fast and inexpensive.
The consequence is a split dependency: retrieval works locally, but a final answer still requires network access to Groq and is subject to its free-tier limits.
The answer path is composed with LangChain Expression Language: retrieve, format documents, build the prompt, call the model, parse the string. LCEL keeps the data flow explicit and makes the empty-retrieval branch easy to isolate.
Vectors are normalized and Chroma uses its default L2 index. The API returns the retrieved passages and metadata, but not a similarity score. Exposing a raw distance without documenting the metric would invite comparisons that do not mean what they appear to mean.
doc_id is the SHA-256 of the file bytes, and each chunk ID combines that hash with its index. Re-uploading identical content replaces the existing document instead of duplicating it.
Replacement upserts the new chunks first and only then prunes IDs that no longer exist, so a failed update does not delete the previous working version first.
Every chunk carries doc_id, filename and chunk_index, plus page metadata supplied by the loader. That is what makes document scoping and page-level citations possible without a second index.
Chroma runs in persistent local mode under data/chroma_db. The embedding model, vector store and LLM client are cached per process, so local development uses a single Uvicorn worker.
Scaling to multiple workers would mean each process loads its own model and opens its own client; that is a deployment concern this local project does not pretend to solve.
If retrieval returns no chunks, the chain returns a fixed message explaining that no relevant information was found. Calling the model with empty context could only produce an ungrounded answer, and it would make an infrastructure failure harder to distinguish from a genuine miss.
Application errors return a consistent JSON shape with a code and message. Provider failures such as rate limits and timeouts are translated before they reach the browser. The frontend renders these as a dismissible banner, inline feedback or a toast — never a browser alert.
FastAPI serves the frontend from the same process at /app, so there is no separate frontend server and no CORS requirement by default. The UI is plain HTML, CSS and JavaScript with no build step.
The normal test suite uses temporary Chroma and upload directories, fake embeddings and a fake chat model. A real-provider test is opt-in through an environment variable. This keeps the suite fast, deterministic and independent of network access.
Chat inference is the only external call. Document processing, embeddings and vector storage run locally.
Being explicit about the boundary is part of the design. Nothing below is implemented today.
Design, backend, frontend and documentation. Built as a complete RAG engineering project with FastAPI, LangChain, ChromaDB, sentence-transformers and Groq.
Model and library credits belong to their respective projects: LangChain, Chroma, Hugging Face sentence-transformers, Groq and FastAPI.