A Retrieval-Augmented Generation (RAG) app built from scratch that lets you ask questions about a PDF and get answers grounded only in that document's content — not the LLM's general training knowledge.
Built as a hands-on portfolio project to demonstrate understanding of the full RAG pipeline: chunking, embeddings, vector search, and grounded generation — using Google's free-tier Gemini API, ChromaDB, and LangChain.
Use case in this repo: a 50-question AI/ML interview prep PDF, turned into an interactive study assistant.
Example:
Q: What is the difference between bagging and boosting?
A: Bagging trains multiple models in parallel on bootstrapped samples and averages their outputs to reduce variance. Boosting trains models sequentially, where each new model focuses on correcting the errors of the previous ones, primarily reducing bias.
Ask something not in the PDF (e.g. "What is a Support Vector Machine?") and the app correctly responds: "I don't have enough information in the document to answer that." — proof the answers are grounded, not hallucinated from the model's general knowledge.
PDF → chunked text → embeddings (vectors) → stored in ChromaDB
↓
Your question → embedding → similarity search → top-k relevant chunks
↓
relevant chunks + question → Gemini → grounded answer
This is the standard two-stage RAG pattern:
- Retrieval — find the most semantically relevant chunks of the source document
- Generation — feed those chunks + the question into an LLM, instructed to answer only from the provided context
rag-pdf-assistant/
├── app.py ← Streamlit UI
├── loader.py ← PDF loading + chunking (pypdf + RecursiveCharacterTextSplitter)
├── embedder.py ← Embeddings (Gemini) + ChromaDB storage
├── retriever.py ← Similarity search over the vector store
├── generator.py ← Prompt construction + Gemini answer generation
├── requirements.txt
└── README.md
Each stage of the pipeline is isolated into its own file so components can be swapped independently — e.g. replacing ChromaDB with Pinecone/FAISS, or the LLM with a different provider, without touching the retrieval or UI logic.
- LangChain — orchestration (document loading, text splitting, vector store interface)
- Google Gemini API (free tier) — embeddings (
gemini-embedding-001) + generation (gemini-2.5-flash-lite) - ChromaDB — local, on-disk vector database (no server, no external API key needed)
- Streamlit — web UI
- pypdf — PDF text extraction
git clone https://github.com/khushikhurana15/rag-pdf-assistant.git
cd rag-pdf-assistantpip install -r requirements.txtCreate a .env file in the project root:
GOOGLE_API_KEY=your_gemini_api_key_here
Get a free key at Google AI Studio.
Place a PDF in the project root and update the filename in loader.py / embedder.py
(default expects ai_ml_interview_qa.pdf).
python embedder.pyThis embeds the PDF's chunks and saves them to a local chroma_db/ folder. You only need
to re-run this if the source PDF changes.
streamlit run app.py- Chunking with overlap (
chunk_size=800,chunk_overlap=150) — prevents cutting a question off from its answer at a chunk boundary while keeping chunks focused enough for precise retrieval. - Low generation temperature (0.2) — keeps answers grounded and consistent rather than creative, which matters for factual Q&A.
- Explicit "say you don't know" instruction in the prompt — a direct, practical hallucination-reduction technique (rather than just hoping the model behaves).
- Adjustable
k(chunks retrieved) exposed in the UI — lets you see live how retrieval breadth trades off against answer focus. - Separation of retrieval and generation into distinct files — retrieval logic (how chunks are found) is fully decoupled from generation logic (how answers are produced), so either can be swapped or upgraded independently.
- No retrieval confidence threshold.
similarity_searchalways returns the top-k chunks even if none are truly relevant. Currently the LLM prompt is relied on to say "I don't know" when context is weak — this works well in practice, but a more robust approach would gate retrieval itself: if the best match's distance score exceeds a threshold, skip calling the LLM entirely and return "not found in document" directly. langchain_community.vectorstores.Chromais deprecated in favor of the standalonelangchain-chromapackage. Still functional as of this writing, but worth migrating.- No re-ranking step. Retrieval is pure vector similarity; a production system might add a cross-encoder re-ranker on top of the initial top-k for higher precision.
- Single PDF, static corpus. No support yet for uploading new PDFs through the UI or incrementally updating the vector store.
- No conversation memory. Each question is answered independently; there's no multi-turn context carried between questions.
MIT — feel free to fork and adapt.
