How it was built and deployed
The backend is a fully async FastAPI application. SQLite (via aiosqlite)
handles persistence for documents, chunks, test sets, questions, attempts, and study
sessions. ChromaDB runs as a PersistentClient with a DATA_DIR
environment variable that switches between local (./data) and Docker
(/data) paths transparently.
Retrieval is handled by a pipeline that pools 8 candidates, applies a distance filter,
runs BM25 reranking, then cross-encoder reranking if enabled. Query decomposition
breaks complex questions into sub-queries before retrieval; HyDE generates a hypothetical
answer to embed as the query instead of the raw question when enabled. Both are
environment-gated so prod behaviour can differ from local without code changes.
PDF highlighting at study citation time is backed by PyMuPDF word-level bounding boxes
extracted at ingest — an 8-word anchor with progressive fallback and empty token
filtering. Bounding boxes are stored as JSON in ChromaDB and delivered to the frontend
via SSE alongside the streamed response. A canvas overlay per page handles hover
preview and click-lock behaviour.
The deployment is a two-stage Docker build: Node builds the React app, Python installs
the backend. FastAPI serves the React static build from the same container — one port,
no CORS concerns. The production LLM switches from local Ollama (Qwen3-8B) to Groq
(Llama 3.3 70B) via a single environment variable. The embedding model switches from
Qwen3-Embedding (Ollama) to BGE-small-en-v1.5 running in-process for CPU compatibility
on the HuggingFace Spaces free tier.
LLM (local)
Qwen3-8B via Ollama
LLM (prod)
Llama 3.3 70B via Groq
Embeddings (prod)
BGE-small-en-v1.5 (in-process)
Vector Store
ChromaDB (PersistentClient)
Chunking
LangChain RecursiveCharacterTextSplitter — 1200 chars, 480 overlap
Reranking
BM25 + ms-marco-MiniLM-L-6-v2 cross-encoder
PDF parsing
PyMuPDF only (no fallback)
Backend
FastAPI (async), SQLite / aiosqlite
Frontend
React (Vite), react-pdf
Deployed on
HuggingFace Spaces (Docker)