Inspiration
I had already written basic RAG code by hand and built agents that reason over data, but I had never had a single tool that answers questions from one document and refuses to guess. Contracts, laws, syllabi and textbooks all share the same problem. You need one specific clause, and a general chatbot will confidently invent it. I wanted an agent whose most important behavior is saying "not found in this document" when the PDF does not contain the answer.
I also wanted to learn how to work with an AI coding agent: give clear requirements, test the result, and tune it until it meets the need.
What it does
Document Q&A Agent lets you upload a PDF (up to 40 MB) and ask questions that are answered only from that document.
- Upload a PDF. It is read, split into chunks, embedded, and stored in a local Chroma vector database.
- Ask in chat. The app retrieves the 7 most similar chunks, and Gemini writes a connected answer using only those chunks.
- Ask something the PDF does not cover (e.g. "Who won the 2018 World Cup?") and you get exactly: not found in this document.
- Summary-style questions ("give me a summary") use chunks sampled evenly across the whole document.
- Conversations are saved, listed in a sidebar (first question and time), and can be continued or deleted.
- Replacing or deleting the PDF makes older conversations read-only, since they belong to a different document.
- Structured documents are split at their headings ("Article 32", "Clause 7", "Unit 2", "Section 4.2"), so "what does clause 7 say" works.
How we built it
- Frontend: React site (port 4000) with a sidebar, drag-and-drop upload, and a chat with waiting indicators and clear error messages.
- Backend: FastAPI (port 8000), organised into:
chunking/reads pages with pypdf (in parallel for large PDFs) and splits with a heading-awareRecursiveCharacterTextSplitter. Each chunk is labelled with its heading.rag/vectorstore.pyembeds chunks locally withbge-small-en-v1.5and stores them in Chroma.rag/chat.pyruns top-7 similarity retrieval, a strict grounding prompt, and Gemini. It tries several Gemini models in order as fallbacks.sessions.pysaves conversations as JSON files.
- Measuring instead of guessing:
tests/eval_retrieval.pytakes a list of questions with known answers and checks whether the answer text is in the top 7 chunks, with no Gemini calls and no quota. Retrieval quality is simply
$$\text{hit rate} = \frac{\text{questions whose answer is in the top-7 chunks}}{\text{total questions}}$$
I ran it before and after every change.
Challenges we ran into
- "Not found" for real questions. "What does article 32 say?" failed on a 402-page Constitution PDF. Embeddings alone could not match article numbers. I tested four chunkings and three embedding models on the same question list. Re-chunking alone did not help (4/14). Heading-aware chunks plus
bge-smallreached 18/20. - Telling two causes of "not found" apart. "Article 457" correctly returned not found because the PDF ends at Article 395. "Article 32" was a real retrieval failure. Measuring was the only way to separate them.
- Speed. Semantic chunking took over 4 minutes on a 402-page PDF. Switching to a recursive splitter, preloading the model and parallelising page reads cut this to about 35–90 seconds.
- Over-fitting to one document. My first chunker was tuned to the Constitution. I rewrote it to recognise numbered and keyword headings in any document, with a plain-splitter fallback. On generated PDFs in five styles (contract, syllabus, textbook, notes, report), heading-aware chunking scored 57/60 vs 48/60 for plain chunking.
- Free-tier limits. Gemini's free tier allows about 20 requests/day per model, so the chat falls back across several models and fails fast on an invalid key.
- Misleading errors. A stopped API was being shown as "this document is invalid". It now says "cannot reach the server".
Accomplishments that we're proud of
- An agent that says "not found in this document" instead of hallucinating. In the build-time test, 5/5 covered questions were answered correctly and 5/5 uncovered questions returned exactly "not found in this document".
- A measured retrieval pipeline, not guesswork: 19/22 on the Constitution and 57/60 across five different document styles.
- Embeddings and vector storage run fully locally. Only the final answer step calls Gemini.
- A 400-page book is ready to question in about 90 seconds, and a 20–50 page PDF in about 10 seconds.
- A complete, polished flow: upload, ask, save, continue, delete, read-only history.
What we learned
- Measure before you change. A fixed question list showed which "improvements" actually helped and which did not.
- Not every miss is a bug. A correct refusal and a retrieval failure look the same in the UI.
- Chunk boundaries matter more than model choice. Labelling chunks with their headings changed results more than swapping retrievers.
- How to work with an AI agent: state requirements precisely, verify each step, decide the trade-offs yourself, and iterate.
What's next for Document Q&A Agent
- Better lookup of near-identical numbered sections (e.g. topic 2.3 vs 2.4) and bare number-only questions like "article 17".
- A query-rewrite step so follow-ups such as "Explain that more simply" work.
- OCR support for scanned PDFs, which are currently reported as invalid.
- Multiple documents, plus citations that show the exact page or section behind each answer.
- Pasted text as an input alongside PDF upload.