← backThe problem
Studying from a document is passive. Turning it into practice questions is the part that actually builds recall, and it is the part nobody has time for. Auto-generated questions usually hallucinate content that was never in the source, and grading a free-text answer by string-matching marks a correct answer wrong because the wording differed.
What I built
A production REST API that ingests a PDF or text file and turns it into an adaptive quiz session grounded entirely in that document. It generates questions, grades free-text answers against a three-criterion rubric with partial credit, tags the specific knowledge gaps behind each wrong answer, and aggregates a session into a report with a grade and a targeted study recommendation.
Architecture
POST /ingest pypdf → 800-char chunks (100 overlap)
→ all-MiniLM-L6-v2 (384-dim) → FAISS
POST /quiz retrieve by document ID → GPT-4o
→ structured JSON questions
POST /eval GPT-4o as judge → 3-criterion rubric
→ partial credit + knowledge-gap tags
GET /report aggregate → grade + ranked gaps
FastAPI · Pydantic v2 · LangChain
Docker container → Railway · React/Vite frontendHighlights
- Every question is grounded in retrieved source chunks, so the model cannot invent material the document never contained.
- Retrieval is scoped by document ID rather than top-k similarity, giving full coverage of the source regardless of how large the index grows.
- Embeddings run 100% locally on all-MiniLM-L6-v2 (384-dim), so embedding cost is $0 and ingestion works offline.
- Rubric grading across accuracy, completeness and terminology, with partial credit and per-criterion feedback.
- Knowledge-gap tags aggregate across a session into ranked weak areas and a study recommendation.
- Six documented REST endpoints with auto-generated OpenAPI docs and Pydantic v2 validation.
- Containerised with Docker and deployed on Railway, built up over 26 incremental commits including real production debugging.
Engineering decisions
Local all-MiniLM-L6-v2 embeddings
instead of A hosted embeddings API
Ingestion embeds every chunk of every uploaded document, the highest-volume operation in the system. Running a small model on CPU takes embedding cost to zero and removes a network dependency from the slowest path, at a quality level that is more than sufficient for retrieving within a single document.
Retrieve all chunks for a document ID
instead of Top-k nearest-neighbour search
Top-k answers 'what is most similar to this query'. Quiz generation needs 'cover this whole document'. With similarity search, a quiz would silently over-sample whatever the query happened to resemble and never ask about the rest of the material.
LLM-as-judge with an explicit rubric
instead of String or keyword matching against a reference answer
A student can be right in words the reference never used, or half-right in a way that deserves partial credit. A rubric across accuracy, completeness and terminology produces a defensible score plus the diagnostic feedback that makes the grade useful. It is the same technique used in reward modelling and model evaluation.
Structured outputs with an enforced JSON schema
instead of Parsing prose responses
Every generation and grading call feeds directly into typed application code. JSON mode with an explicit schema plus Pydantic validation means a malformed response fails loudly at the boundary instead of corrupting a quiz halfway through a session.
Stack
- Python
- FastAPI
- LangChain
- FAISS
- GPT-4o
- sentence-transformers
- Pydantic v2
- Docker
- Railway
- React
- Vite
- TypeScript