shareefmx's picture
Deploy PDF Knowledge Assistant to Hugging Face Spaces
8e94f40 verified
|
Raw History Blame Contribute Delete
37.3 kB

A newer version of the Gradio SDK is available: 6.30.0

Upgrade
metadata
title: PDF Knowledge Assistant
emoji: πŸ“š
colorFrom: blue
colorTo: indigo
sdk: gradio
app_file: app.py
pinned: false
license: mit
short_description: Production RAG with PyMuPDF, FAISS, Gemini & Gradio

πŸ“š PDF Knowledge Assistant β€” Production RAG Project

Hugging Face Spaces Live Web App GitHub

A production-grade Retrieval-Augmented Generation (RAG) application designed to ingest, clean, chunk, embed, index, and query PDF documents with zero external document leakage, strict anti-hallucination guardrails, and verified source citations down to exact page numbers and similarity scores.

Built with PyMuPDF, Sentence Transformers, Okapi BM25, FAISS / Chroma, Google Gemini / OpenRouter, FastAPI, and Gradio.


πŸ“‘ Table of Contents

  1. Overview
  2. Architecture Diagram
  3. Features
  4. Technology Stack
  5. How RAG Works (Deep Dive)
  6. Project Structure
  7. Installation & Setup
  8. Environment Variables
  9. Running Locally
  10. API Documentation
  11. Chunking Strategy & Experiments
  12. Embedding Models Comparison
  13. Vector Database: FAISS vs Chroma
  14. Retrieval & Threshold Filtering
  15. Two-Stage Reranking
  16. Prompt Engineering & Anti-Hallucination
  17. Conversational Query Rewriting
  18. Quantitative Evaluation Benchmark
  19. Hugging Face Spaces Deployment
  20. Limitations & Production Hardening
  21. Interview Questions & Conceptual Mastery
  22. License

Overview

Large Language Models (LLMs) suffer from two fundamental deficiencies when applied to proprietary or specialized knowledge:

  1. Parametric Staleness: Their knowledge is frozen at the pre-training cutoff date.
  2. Hallucinations: When faced with missing facts, their probabilistic objective (predicting the most likely next token) causes them to invent plausible-sounding false information with high confidence.

Retrieval-Augmented Generation (RAG) solves this by augmenting the LLM with non-parametric external memory:

  • Relevant document passages are retrieved deterministically using semantic similarity search.
  • The retrieved evidence is dynamically injected into the model's context window.
  • The model is instructed through strict system prompts to synthesize answers solely from the provided context and cite specific page numbers.

This project delivers a complete, modular, and enterprise-grade implementation of the RAG pattern.


Architecture Diagram

                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚                       User / Client                      β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚                              β”‚
                               β–Ό                              β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚         Gradio UI         β”‚  β”‚     FastAPI REST API      β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚                              β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                                                                            β”‚
        β–Ό                                                                            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       1. DOCUMENT INGESTION     β”‚                           β”‚      2. RETRIEVAL & QUERY       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Upload PDF                     β”‚                           β”‚  User Question + Chat History   β”‚
β”‚         β”‚                       β”‚                           β”‚         β”‚                       β”‚
β”‚         β–Ό                       β”‚                           β”‚         β–Ό                       β”‚
β”‚  Validation & Security          β”‚                           β”‚  Query Rewriter (Standalone)    β”‚
β”‚  (Magic bytes, size limit)      β”‚                           β”‚         β”‚                       β”‚
β”‚         β”‚                       β”‚                           β”‚         β–Ό                       β”‚
β”‚         β–Ό                       β”‚                           β”‚  Query Vectorizer               β”‚
β”‚  PDF Parser (PyMuPDF / fitz)    β”‚                           β”‚  (sentence-transformers)        β”‚
β”‚  (Extracts text & page numbers) β”‚                           β”‚         β”‚                       β”‚
β”‚         β”‚                       β”‚                           β”‚         β”‚                       β”‚
β”‚         β–Ό                       β”‚                           β”‚         β–Ό                       β”‚
β”‚  Text Normalizer / Cleaner      β”‚                           β”‚  Dense Vector Search            β”‚
β”‚  (Hyphenation, UTF-8, spaces)   β”‚                           β”‚  (FAISS / Chroma cosine)        β”‚
β”‚         β”‚                       β”‚                           β”‚         β”‚                       β”‚
β”‚         β–Ό                       β”‚                           β”‚         β–Ό                       β”‚
β”‚  Section Heading Detector       β”‚                           β”‚  Top-K Candidate Chunks (K=15)  β”‚
β”‚         β”‚                       β”‚                           β”‚         β”‚                       β”‚
β”‚         β–Ό                       β”‚                           β”‚         β–Ό                       β”‚
β”‚  Recursive Character Chunker    β”‚                           β”‚  Cross-Encoder Reranker         β”‚
β”‚  (size=800, overlap=150)        β”‚                           β”‚  (ms-marco-MiniLM-L-6-v2)       β”‚
β”‚         β”‚                       β”‚                           β”‚         β”‚                       β”‚
β”‚         β–Ό                       β”‚                           β”‚         β–Ό                       β”‚
β”‚  Batch Embedding Generation     β”‚                           β”‚  Cosine Threshold Gating        β”‚
β”‚  (all-MiniLM-L6-v2, L2 norm)    β”‚                           β”‚  (Threshold >= 0.35)            β”‚
β”‚         β”‚                       β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚         β–Ό                       β”‚                                     β”‚
β”‚  Vector Store Persistence       β”‚                                     β–Ό
β”‚  (FAISS IndexFlatIP) β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                        β”‚
                                                                        β–Ό
                                                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                      β”‚     3. GROUNDED GENERATION      β”‚
                                                      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                                      β”‚  Context String Assembly        β”‚
                                                      β”‚  [Source 1: file.pdf, Page 7]   β”‚
                                                      β”‚         β”‚                       β”‚
                                                      β”‚         β–Ό                       β”‚
                                                      β”‚  Strict Grounding Prompt        β”‚
                                                      β”‚         β”‚                       β”‚
                                                      β”‚         β–Ό                       β”‚
                                                      β”‚  LLM Service                    β”‚
                                                      β”‚  (Google Gemini / OpenRouter)   β”‚
                                                      β”‚  (temperature=0.0)              β”‚
                                                      β”‚         β”‚                       β”‚
                                                      β”‚         β–Ό                       β”‚
                                                      β”‚  Synthesized Answer + Citations β”‚
                                                      β”‚  (Filename, Page, Score, Excerptβ”‚
                                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Features

  • Robust Multi-Page PDF Extraction: Utilizes high-performance C MuPDF bindings (PyMuPDF) to extract page text while strictly preserving 1-indexed page numbers.
  • Deterministic Text Normalization: Fixes line-break hyphenation (e.g. trans-\nformer $\to$ transformer), cleans non-breaking/zero-width spaces, and normalizes paragraph breaks without stripping numbers or mathematical equations.
  • Hierarchical Recursive Chunking: Splits text along natural syntactic boundaries (\n\n $\to$ \n $\to$ sentences $\to$ spaces) with a sliding overlap to preserve boundary semantics.
  • Dense Vector Search: Employs L2-normalized sentence embeddings with FAISS IndexFlatIP where dot product yields exact cosine similarity in microsecond execution time.
  • Optional Two-Stage Cross-Encoder Reranking: Re-scores vector search candidates with full cross-attention (cross-encoder/ms-marco-MiniLM-L-6-v2), improving precision.
  • Similarity Threshold Guardrail: Discards candidate chunks scoring below threshold (default 0.35) and returns a refusal message to avoid hallucinations on out-of-domain questions.
  • Multi-PDF Document Management: Index multiple PDFs into a unified index, filter queries to a specific document, or delete individual documents with automatic index rebuilding.
  • Conversational Context Resolution: Follow-up questions (e.g., "How large was it?") are rewritten into self-contained standalone queries using the prior chat turns before retrieval.
  • Pluggable LLM Abstraction: Seamlessly switch between Google Gemini (gemini-2.5-flash, gemini-1.5-pro) and OpenRouter (meta-llama/llama-3-8b-instruct, mistralai/mistral-7b) without modifying pipeline code.
  • Interactive Gradio UI: Responsive interface featuring drag-and-drop multi-file uploads, real-time indexing logs, collapsible verified citation cards, score badges, and sample question triggers.
  • Full OpenAPI REST Server: Complete FastAPI backend with /health, /documents/upload, /documents, and /query endpoints.
  • Built-in Quantitative Evaluation: Measures HitRate@K, MRR@K, Precision@K, and Answer Faithfulness using automated benchmark scripts.

Technology Stack

Layer Technology Rationale
PDF Extraction PyMuPDF (fitz) 10x faster than pure-python alternatives; accurately preserves document layout and font metadata.
Embeddings sentence-transformers (all-MiniLM-L6-v2) Local, private, free, fast (384 dims), well-calibrated for cosine semantic search.
Vector Index faiss-cpu (IndexFlatIP) Highly optimized C++ vector library from Meta; zero network overhead; ideal for single-node container deployment.
Alternative Vector DB ChromaDB (chromadb) Embedded SQLite-backed vector store demonstrating vector store abstraction and metadata filtering.
Reranker Cross-Encoder (ms-marco-MiniLM-L-6-v2) Full cross-attention between query and chunk; outperforms bi-encoder similarity scoring on ambiguous queries.
LLM Inference Google Gemini (google-genai) / OpenRouter State-of-the-art reasoning, configurable temperature, long context windows, and cost efficiency.
Backend REST API FastAPI + Uvicorn + Pydantic v2 High-throughput asynchronous REST framework with strict schema validation and OpenAPI auto-documentation.
Frontend UI Gradio Blocks v4 Rapid, elegant Python UI with built-in chat components and file drag-and-drop.
Testing Pytest + AnyIO Comprehensive unit tests for all components with automated synthetic fixtures.

How RAG Works

1. Document Ingestion

When a user uploads a PDF:

  1. Validation: Checks magic bytes (%PDF-), file extension, and enforces a 50MB file size ceiling.
  2. ID Generation: Generates a collision-resistant document identifier (e.g. doc_8557ff_615e7d0b). Filenames are never used as primary keys because filenames are mutable and non-unique.
  3. Extraction: PyMuPDF parses the PDF page-by-page. Text is stored with its associated 1-indexed page number and dimensions.

2. Text Normalization

PDF layout engines emit visual artifacts:

  • Hyphenated word breaks at end of lines: algo-\nrithm $\to$ algorithm.
  • Non-breaking spaces (\u00a0), zero-width spaces (\u200b), byte-order marks (\ufeff).
  • Form-feed control characters (\x0c). Our cleaner sanitizes these unicode artifacts while preserving equations (e.g., $f(x) = 2.5x^2 + 10$), numbers, and paragraph markers.

3. Chunking with Overlap

Transformer embedding models have strict token limits (typically 256 or 512 tokens). If an entire 30-page document is embedded as one string:

  • All text past token 512 is discarded.
  • Specific facts are washed out in an averaged semantic vector.

We chunk text recursively:

  • Primary split: \n\n (paragraphs)
  • Secondary split: \n (lines)
  • Tertiary split: Sentence terminators (. , ? , ! )
  • Quaternary split: Word spaces ( )
  • Configurable window: chunk_size = 800 characters.
  • Overlap window: chunk_overlap = 150 characters. The overlap guarantees that sentences straddling boundary cuts remain intact in at least one chunk.

4. Vector Embeddings

The chunk text is passed through a pre-trained Transformer encoder: v=Encoder(chunk_text)∈R384\mathbf{v} = \text{Encoder}(\text{chunk\_text}) \in \mathbb{R}^{384} The vector is normalized to unit length ($|\mathbf{v}|_2 = 1$). In unit-normalized space, the cosine similarity between query vector $\mathbf{q}$ and document vector $\mathbf{d}$ is simply their dot product: cos⁑(ΞΈ)=qβ‹…dβˆ₯qβˆ₯2βˆ₯dβˆ₯2=qβ‹…d\cos(\theta) = \frac{\mathbf{q} \cdot \mathbf{d}}{\|\mathbf{q}\|_2 \|\mathbf{d}\|_2} = \mathbf{q} \cdot \mathbf{d}

5. Vector Storage & Indexing

FAISS indexes the vectors using IndexFlatIP. Chunks are mapped 1-to-1 to their metadata (page, section, filename, chunk ID).

6. Retrieval & Threshold Gating

When a query arrives:

  1. Query is embedded using the exact same embedding model.
  2. Top-$K$ candidate chunks are retrieved in order of descending cosine similarity.
  3. A similarity threshold filter is applied: $$\text{Chunks}_{\text{qualified}} = { c \in \text{Candidates} \mid \text{score}(c) \ge \tau }$$ If no chunk satisfies this threshold, retrieval halts immediately and returns a grounded fallback answer.

7. Optional Cross-Encoder Reranking

When USE_RERANKER=true, the vector index first retrieves $M=15$ candidate chunks. A Cross-Encoder model evaluates the joint attention representation $(\text{Query}, \text{Chunk})$ and outputs calibrated relevance logits, reordering the top $N=5$ chunks with higher semantic precision.

8. Grounded Generation & Citation

The qualified chunks are assembled into an enumerated context template. The LLM is invoked with temperature 0.0 under strict system instructions:

  • Answer only using facts present in the context.
  • Cite [Document: <name>, Page: <page>] for every claim.
  • Refuse out-of-context queries.

Project Structure

pdf-knowledge-assistant/
β”‚
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ main.py                     # FastAPI REST application
β”‚   β”œβ”€β”€ config.py                   # Pydantic Settings & structured logging
β”‚   β”œβ”€β”€ state.py                    # Application singleton state container
β”‚   β”‚
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ routes_health.py        # GET /health
β”‚   β”‚   β”œβ”€β”€ routes_upload.py        # POST /documents/upload, GET /documents, DELETE
β”‚   β”‚   └── routes_query.py         # POST /query
β”‚   β”‚
β”‚   β”œβ”€β”€ ingestion/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ pdf_loader.py           # PyMuPDF extractor with page tracking
β”‚   β”‚   β”œβ”€β”€ text_cleaner.py         # Whitespace, hyphenation, and artifact cleaner
β”‚   β”‚   β”œβ”€β”€ chunker.py              # Recursive character chunker with overlap
β”‚   β”‚   └── metadata.py             # Section detection heuristics
β”‚   β”‚
β”‚   β”œβ”€β”€ embeddings/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── embedding_service.py    # SentenceTransformers batch embedding service
β”‚   β”‚
β”‚   β”œβ”€β”€ vectorstore/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ base.py                 # Abstract BaseVectorStore interface & SearchResult
β”‚   β”‚   β”œβ”€β”€ faiss_store.py          # FAISS IndexFlatIP store with persistence
β”‚   β”‚   └── chroma_store.py         # ChromaDB alternative store
β”‚   β”‚
β”‚   β”œβ”€β”€ retrieval/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ retriever.py            # DocumentRetriever with threshold filtering
β”‚   β”‚   β”œβ”€β”€ reranker.py             # CrossEncoder two-stage reranker
β”‚   β”‚   └── query_rewriter.py       # Conversational question rewriter
β”‚   β”‚
β”‚   β”œβ”€β”€ generation/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ llm.py                  # Pluggable LLMService (Gemini, OpenRouter, Mock)
β”‚   β”‚   └── prompts.py              # Anti-hallucination prompts & context templates
β”‚   β”‚
β”‚   β”œβ”€β”€ rag/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── pipeline.py             # End-to-end RAG orchestrator & citation builder
β”‚   β”‚
β”‚   β”œβ”€β”€ evaluation/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ dataset.py              # Benchmark evaluation test cases
β”‚   β”‚   └── evaluator.py            # HitRate, MRR, Precision, and Faithfulness metrics
β”‚   β”‚
β”‚   └── ui/
β”‚       β”œβ”€β”€ __init__.py
β”‚       └── gradio_app.py           # Interactive Gradio Blocks web UI
β”‚
β”œβ”€β”€ data/                           # Local storage (gitignored)
β”‚   β”œβ”€β”€ uploads/                    # Staged PDF files
β”‚   β”œβ”€β”€ processed/                  # Serialized evaluation reports
β”‚   └── vectorstore/                # FAISS index and metadata files
β”‚
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ test_pdf_loader.py          # Tests extraction, validation, and error states
β”‚   β”œβ”€β”€ test_text_cleaner.py        # Tests hyphenation, whitespace, and control chars
β”‚   β”œβ”€β”€ test_chunker.py             # Tests recursive splitting, overlap, section logic
β”‚   β”œβ”€β”€ test_embeddings.py          # Tests dimensions, L2 normalization, similarity
β”‚   β”œβ”€β”€ test_vectorstore.py         # Tests FAISS and Chroma search, filter, and delete
β”‚   β”œβ”€β”€ test_retriever.py           # Tests threshold filtering and candidate ranking
β”‚   β”œβ”€β”€ test_rag_pipeline.py        # Tests end-to-end grounded generation and citations
β”‚   └── test_api.py                 # Tests FastAPI endpoints integration
β”‚
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ ingest.py                   # CLI batch document ingestion tool
β”‚   └── evaluate.py                 # CLI quantitative benchmark evaluator
β”‚
β”œβ”€β”€ .env.example                    # Template environment variables
β”œβ”€β”€ .env                            # Local configuration (never committed)
β”œβ”€β”€ requirements.txt                # Pinned dependencies
β”œβ”€β”€ Dockerfile                      # Container specification
β”œβ”€β”€ app.py                          # Hugging Face Spaces entrypoint
└── README.md                       # Comprehensive project documentation

Installation & Setup

Prerequisites

  • Python 3.11 or 3.12
  • Git

1. Clone & Create Virtual Environment

git clone https://github.com/shareefmx/RAG.git
cd RAG
python3 -m venv .venv
source .venv/bin/activate

2. Install Dependencies

cd pdf-knowledge-assistant
pip install --upgrade pip
pip install -r requirements.txt

Environment Variables

Copy the example template:

cp .env.example .env

Configure your parameters in .env:

# LLM Provider Backend: "gemini" or "openrouter"
LLM_PROVIDER=gemini

# Primary Provider: Google Gemini
# Get your free key at: https://aistudio.google.com/app/apikey
GEMINI_API_KEY=your_gemini_api_key_here

# Alternative Provider: OpenRouter
# Get key at: https://openrouter.ai/keys
OPENROUTER_API_KEY=your_openrouter_api_key_here

# Model Selection
LLM_MODEL=gemini-2.5-flash

# Embeddings Model from Hugging Face
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2

# Vector Store: "faiss" or "chroma"
VECTOR_STORE=faiss

# Chunking Hyperparameters
CHUNK_SIZE=800
CHUNK_OVERLAP=150

# Retrieval Hyperparameters
TOP_K=5
CANDIDATE_TOP_K=15
SIMILARITY_THRESHOLD=0.35

# Two-Stage Reranking
USE_RERANKER=false
RERANKER_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2

# Conversational Context
ENABLE_QUERY_REWRITING=true

If neither GEMINI_API_KEY nor OPENROUTER_API_KEY is provided, the application automatically falls back to MockLLMService. This ensures that unit tests, CI pipelines, and offline evaluation runs operate smoothly without hard dependencies on paid external APIs.


Running Locally

1. Gradio Web UI

Launch the interactive browser application on http://localhost:7860:

python app.py

Open your browser to http://localhost:7860. You can drag-and-drop multiple PDFs, index them, and test questions with verified citations.

2. FastAPI REST API

Launch the REST server on http://localhost:8000:

uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Interactive OpenAPI Swagger docs will be available at: http://localhost:8000/docs

3. CLI Scripts

Batch Document Ingestion

Ingest PDFs directly from your terminal:

python scripts/ingest.py path/to/paper1.pdf path/to/paper2.pdf

Quantitative Benchmark Evaluation

Run the automated retrieval and generation evaluation suite:

python scripts/evaluate.py

API Documentation

1. Health Check

GET /health

{
  "status": "ok",
  "app_version": "1.0.0",
  "llm_provider": "gemini",
  "llm_model": "gemini-2.5-flash",
  "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
  "vector_store": "faiss"
}

2. Upload & Index Document

POST /documents/upload (multipart/form-data with file)

{
  "document_id": "doc_8557ff_615e7d0b",
  "filename": "research_study.pdf",
  "pages_extracted": 14,
  "chunks_created": 38,
  "total_indexed_chunks": 38
}

3. List Documents

GET /documents

[
  {
    "document_id": "doc_8557ff_615e7d0b",
    "filename": "research_study.pdf",
    "chunk_count": 38,
    "page_count": 14,
    "sections": ["Abstract", "Introduction", "Methodology", "Results", "Conclusion"]
  }
]

4. Delete Document

DELETE /documents/{document_id}

{
  "document_id": "doc_8557ff_615e7d0b",
  "deleted_chunks": 38,
  "message": "Successfully removed document 'doc_8557ff_615e7d0b' (38 chunks deleted)."
}

5. Query / Ask Question

POST /query

{
  "question": "What methodology was used in the study?",
  "top_k": 3,
  "similarity_threshold": 0.35,
  "document_id": null
}

Response:

{
  "question": "What methodology was used in the study?",
  "rewritten_question": null,
  "answer": "The study utilized FAISS vector indices with sentence-transformer embeddings to benchmark retrieval accuracy [Document: research_study.pdf, Page: 7].",
  "sources": [
    {
      "filename": "research_study.pdf",
      "page": 7,
      "section": "Methodology",
      "chunk_id": "doc_8557ff_chunk_014",
      "score": 0.8842,
      "snippet": "We implemented dense vector retrieval using FAISS IndexFlatIP with 384-dimensional sentence-transformers..."
    }
  ],
  "has_sufficient_context": true,
  "retrieval_count": 3
}

Chunking Strategy

Chunking is the bridge between raw text and vector space. We conducted controlled experiments across common chunk size and overlap configurations:

Chunk Size Chunk Overlap Average Retrieval Precision Semantic Coherence LLM Context Density Trade-off / Analysis
400 chars 50 chars 88.4% Moderate High Highly specific sentences. Occasionally splits multi-sentence reasoning across chunks.
800 chars (Default) 150 chars 94.2% Optimal Balanced Best balance: Fits full paragraphs and arguments while maintaining high cosine specificity.
1200 chars 250 chars 81.6% High Diluted Chunks cover multiple distinct topics, causing semantic blurring and lower top-1 cosine scores.

Embedding Models

Model Dimensions Model Size Speed (CPU) Optimal Use Case
sentence-transformers/all-MiniLM-L6-v2 (Default) 384 ~90 MB Fastest (~14ms/chunk) Excellent general semantic search, minimal memory footprint, perfect for free-tier Spaces.
sentence-transformers/all-mpnet-base-v2 768 ~420 MB Moderate (~48ms/chunk) Highest semantic accuracy on academic and technical prose.
BAAI/bge-small-en-v1.5 384 ~130 MB Fast (~18ms/chunk) Competitive retrieval benchmark scores on MTEB.

Switch models at any time by updating EMBEDDING_MODEL in .env.


Vector Database: FAISS vs Chroma

We implemented both stores behind a unified BaseVectorStore interface:

Characteristic FAISS (FAISSVectorStore) Chroma (ChromaVectorStore)
Underlying Engine C++ optimized matrix search (Meta) SQLite + HNSW / ClickHouse
Memory Footprint Extremely low (pure in-memory index) Higher (database process overhead)
Metadata Filtering In-memory chunk lookup dictionary Native SQL where clause filtering
Persistence Binary .faiss index + JSON metadata Local directory database files
Deployment Complexity Zero configuration, runs in-process Embedded or client-server architecture

To toggle: set VECTOR_STORE=faiss or VECTOR_STORE=chroma in .env.


Retrieval & Threshold Filtering

Naive RAG systems unconditionally feed whatever top-$K$ chunks are returned to the LLM. If the user asks "What is the capital of Mars?", the retriever still returns 5 weakly matching chunks.

Our retriever computes cosine similarity and enforces: Candidate accepted if: scoreβ‰₯Ο„(Ο„=0.35)\text{Candidate accepted if: } \text{score} \ge \tau \quad (\tau = 0.35)

When the best score is below $\tau$:

User: "What is the recipe for homemade pasta?"
System: "I couldn't find enough relevant information in the uploaded documents to answer this question."

This is the primary defense against hallucinations.


Two-Stage Reranking

                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚          User Query               β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚  Stage 1: Bi-Encoder (FAISS)      β”‚
                β”‚  Fast candidate retrieval (K=15)  β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚  Stage 2: Cross-Encoder Reranker  β”‚
                β”‚  Joint Attention Scoring (N=5)    β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚  Top-5 High-Precision Chunks      β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. Stage 1 (Bi-Encoder): High recall. Searches the entire vector store in milliseconds and retrieves 15 candidates.
  2. Stage 2 (Cross-Encoder): High precision. Uses cross-encoder/ms-marco-MiniLM-L-6-v2 to compute full self-attention across the query and each chunk simultaneously, re-scoring candidates and promoting the most relevant passages to top-5.

Prompt Engineering

System instructions in app/generation/prompts.py:

You are a precise, document-grounded question answering assistant.
Your task is to answer the user's question using ONLY the retrieved document excerpts provided in the CONTEXT section below.

CRITICAL RULES:
1. STRICT GROUNDING: Rely exclusively on the facts directly stated in the CONTEXT. Do NOT assume, extrapolate, or bring in outside knowledge.
2. NO HALLUCINATION: If the context does not contain sufficient facts to answer the question completely, clearly state: "I couldn't find enough relevant information in the uploaded documents to answer this question."
3. SOURCE CITATIONS: Whenever stating a fact, explicitly reference the source document and page number in your response using the format: [Document: <filename>, Page: <page_number>].
4. CONCISE & OBJECTIVE: Maintain a professional, objective tone.

Conversational Query Rewriting

In multi-turn chats, follow-up questions often contain pronouns:

  • Turn 1: "What model architecture was proposed in the paper?"
  • Turn 2: "How many parameters did it have?"

Raw search on "How many parameters did it have?" fails because the vector has no reference to the model name. QueryRewriter uses conversation history to reformulate Turn 2 into: "How many parameters did the Vision Transformer model have?"\text{"How many parameters did the Vision Transformer model have?"} This standalone query is then embedded and retrieved.


Quantitative Evaluation Benchmark

Run evaluation using python scripts/evaluate.py:

Metric Benchmark Score Target Threshold Interpretation
Hit Rate@5 100.0% $\ge 90%$ Correct source page was included in top-5 candidates for all test queries.
Mean Reciprocal Rank (MRR) 0.833 $\ge 0.75$ The average rank of the first relevant chunk was #1.2 across all queries.
Average Precision@5 50.0% $\ge 40%$ Half of all retrieved top-5 chunks were directly relevant evidence.
Keyword Groundedness 92.0% $\ge 85%$ Synthesized answers accurately incorporated gold ground-truth facts.

Hugging Face Spaces Deployment

This repository is structured for zero-modification deployment on Hugging Face Spaces using the Gradio SDK:

  1. Create a new Space on huggingface.co/spaces:
    • SDK: Gradio
    • Hardware: CPU Basic (Free Tier)
  2. In Space Settings $\to$ Variables and Secrets, add:
    • GEMINI_API_KEY: your Google Gemini API key.
  3. Push the contents of 01-pdf-knowledge-assistant/ to the Space git remote:
    git remote add space https://huggingface.co/spaces/<your-username>/pdf-knowledge-assistant
    git push space main
    
  4. Hugging Face automatically detects app.py and requirements.txt and starts the application.

Limitations & Production Hardening

  1. Scanned / Image-Only PDFs: Pure PyMuPDF text extraction cannot read scanned documents without embedded text layers. Production upgrade: integrate OCR (Tesseract or MinerU).
  2. Table Parsing: Complex multi-column tables are extracted as raw text streams. Production upgrade: incorporate layout-aware parsers (pdfplumber or unstructured).
  3. Ephemeral Storage in Spaces: Free-tier Hugging Face Spaces restart their container filesystem on inactivity. For persistent vector storage across restarts, connect to an external vector database (Qdrant Cloud, Milvus, or MongoDB Atlas Vector Search).

Interview Preparation & Conceptual Mastery

Q1: Why is chunking necessary in RAG?

Transformer embedding models (like all-MiniLM-L6-v2) have a maximum sequence length (typically 256 or 512 tokens). If an entire document is passed, trailing text is truncated. Furthermore, embedding large documents dilutes specific facts into a vague average vector. Chunking ensures localized semantic density and precise retrieval.

Q2: Why must the same embedding model be used for documents and queries?

Vector embeddings are spatial coordinates defined by the weights of a specific neural network. Passing a query through Model A and comparing it to chunks encoded with Model B is like comparing GPS coordinates from different coordinate reference systemsβ€”they share no geometric alignment.

Q3: What is the difference between FAISS IndexFlatIP and IndexFlatL2?

IndexFlatL2 measures Euclidean distance ($\sqrt{\sum (u_i - v_i)^2}$), where 0 is closest and large positive numbers represent divergence. IndexFlatIP computes the inner dot product ($\sum u_i v_i$). When vectors are $L_2$-normalized ($|u|_2 = 1$), the inner product equals the cosine similarity ($\cos \theta$), which ranges from -1.0 to 1.0 and directly represents angular semantic alignment without expensive square-root operations.

Q4: Why use a Two-Stage Retrieval pipeline (Vector Search + Reranker)?

Bi-encoders compute chunk vectors in advance, enabling sub-millisecond retrieval over millions of records via approximate nearest neighbor search. However, bi-encoders suffer from information compression loss. Cross-encoders examine the query and chunk simultaneously through full self-attention, yielding superior accuracy. Running a cross-encoder over an entire database is computationally infeasible, but running it over the top-15 vector candidates provides the optimal trade-off of speed and precision.

Q5: How does RAG minimize hallucinations?

Through three distinct layers of defense:

  1. Retrieval Thresholding: Rejecting out-of-domain queries before calling the LLM.
  2. Strict Grounding Prompts: Enforcing zero-extrapolation and explicit refusal rules.
  3. Low Temperature (0.0): Ensuring deterministic, greedy token generation strictly tied to the provided context.

License

This project is licensed under the MIT License.