Spaces:
Sleeping
Sleeping
| title: RAG Document Q&A | |
| emoji: π | |
| colorFrom: indigo | |
| colorTo: blue | |
| sdk: docker | |
| app_port: 7860 | |
| pinned: false | |
| # RAG Document Q&A System | |
| Upload PDF documents and get cited, grounded answers β with hybrid retrieval, cross-encoder re-ranking, and confidence-aware generation. | |
| ## Overview | |
| This project implements a production-grade Retrieval-Augmented Generation pipeline that goes beyond the typical "embed + nearest-neighbor + prompt" tutorial. It combines semantic and keyword search with cross-encoder re-ranking, applies Anthropic's contextual retrieval pattern to enrich embeddings with document metadata, and gates LLM calls on retrieval confidence to avoid hallucinated answers. The system ships with both a streaming Gradio UI and a FastAPI backend with Server-Sent Events, plus an evaluation harness that benchmarks chunking strategies and scores end-to-end answer quality using an LLM-as-judge. | |
| ## Architecture | |
| ``` | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| β INGESTION β | |
| β β | |
| PDF βββΊ pymupdf4llm βββΊ β Chunking (3 strategies) β | |
| (pdfplumber β ββ fixed_size sliding window β | |
| fallback) β ββ recursive_char LangChain splits β | |
| β ββ semantic embedding sim β | |
| β β β | |
| β βΌ β | |
| β Contextual Enrichment β | |
| β "filename | Page N | Section\ntext" β | |
| β β β | |
| β βΌ β | |
| β all-MiniLM-L6-v2 (384-dim, L2-norm) β | |
| β β β | |
| β βββββββ΄βββββββ β | |
| β βΌ βΌ β | |
| β FAISS BM25Okapi β | |
| β IndexFlatIP rank-bm25 β | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| β QUERY β | |
| β β | |
| Question βββββββββββββββΊβ Adaptive Expansion (Groq) β | |
| β only if top_score < 0.45 β | |
| β β β | |
| β βΌ β | |
| β Hybrid Search β | |
| β score = 0.7Β·semantic + 0.3Β·bm25 β | |
| β (top 20 candidates) β | |
| β β β | |
| β βΌ β | |
| β Cross-Encoder Re-ranking β | |
| β ms-marco-MiniLM-L-6-v2 β | |
| β (20 β top k) β | |
| β β β | |
| β βΌ β | |
| β Confidence Gating β | |
| β cosine sim < 0.3 β skip LLM β | |
| β β β | |
| β βΌ β | |
| β Groq (Llama 3.3 70B) streaming β | |
| β β β | |
| β βΌ β | |
| β Cited answer [Source N] notation β | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| β EVALUATION β | |
| β β | |
| β Retrieval: Acc@1/3/5, MRR β | |
| β End-to-end: faithfulness + relevance β | |
| β (LLM-as-judge, temp=0.0) β | |
| β Config sweep: chunking strategy grid β | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| ``` | |
| ## Key Features | |
| - **Cross-encoder re-ranking** β bi-encoder retrieval (FAISS) produces 20 candidates; a cross-encoder (`ms-marco-MiniLM-L-6-v2`) scores each `(query, chunk)` pair jointly and re-orders them. Cross-encoders are too slow for full-corpus search but dramatically improve precision over the shortlist. | |
| - **Hybrid search** β FAISS inner-product search and BM25 run in parallel. Both score sets are normalized to `[0, 1]` then fused (`0.7Β·semantic + 0.3Β·keyword`), catching exact-match terms that dense embeddings tend to smooth over. | |
| - **Adaptive query expansion** β the pipeline runs an initial search first. Only if the top cosine similarity falls below 0.45 does it call the LLM to generate 3 rephrased variants, search each, and merge results. High-confidence queries pay zero expansion cost. | |
| - **Contextual chunk enrichment** β each chunk is embedded as `"<filename> | Page N | <section>\n<text>"` rather than raw text, following Anthropic's contextual retrieval pattern. The embedding captures document location and section context, not just lexical content. | |
| - **Confidence-based response gating** β `max_score` is taken from cosine similarity *before* re-ranking (cross-encoder scores on a β10 to +10 scale would break the threshold). If `max_score < 0.3`, the LLM is skipped entirely and the user receives an honest "insufficient context" message. Scores between 0.3β0.5 append a low-confidence warning. | |
| - **Streaming responses** β Gradio UI streams tokens via a generator; the FastAPI backend streams as Server-Sent Events (`text/event-stream`) with `{"text": chunk}` events and a terminal `{"done": true}`. | |
| - **Layout-aware PDF parsing** β `pymupdf4llm` extracts structured markdown with headings and tables preserved. `pdfplumber` serves as a fallback for PDFs that `pymupdf4llm` cannot parse cleanly. | |
| - **End-to-end evaluation** β the eval harness measures retrieval hit rate (Acc@K, MRR), then pipes results through the LLM-as-judge to score faithfulness (are claims grounded in context?) and relevance (does the answer address the question?) at temperature 0 for determinism. | |
| ## Tech Stack | |
| | Component | Technology | | |
| |---|---| | |
| | Embedding model | `all-MiniLM-L6-v2` (sentence-transformers, 384-dim) | | |
| | Vector index | FAISS `IndexFlatIP` (exact inner-product search) | | |
| | Keyword index | BM25 (`rank-bm25`, `BM25Okapi`) | | |
| | Re-ranker | `cross-encoder/ms-marco-MiniLM-L-6-v2` | | |
| | LLM | Llama 3.3 70B via Groq API | | |
| | PDF parsing | `pymupdf4llm` + `pdfplumber` fallback | | |
| | Text splitting | LangChain `RecursiveCharacterTextSplitter` | | |
| | Backend | FastAPI + Uvicorn | | |
| | Frontend | Gradio (Blocks, streaming) | | |
| | Containerization | Docker | | |
| | Cloud deployment | HuggingFace Spaces | | |
| ## Quick Start | |
| ```bash | |
| git clone <repo-url> | |
| cd rag-qa | |
| python -m venv venv && source venv/bin/activate | |
| pip install -r requirements.txt | |
| ``` | |
| Create a `.env` file: | |
| ``` | |
| GROQ_API_KEY=your_key_here | |
| ``` | |
| Get a free key at [console.groq.com](https://console.groq.com) β the free tier allows 14,400 requests/day. | |
| Launch the Gradio UI: | |
| ```bash | |
| python app_gradio.py | |
| # Opens at http://127.0.0.1:7860 | |
| ``` | |
| Or run the FastAPI server: | |
| ```bash | |
| uvicorn main:app --reload | |
| # API docs at http://127.0.0.1:8000/docs | |
| ``` | |
| ## API Endpoints | |
| | Method | Endpoint | Description | | |
| |---|---|---| | |
| | `GET` | `/health` | Liveness check | | |
| | `GET` | `/stats` | Index size, chunks ingested, model info | | |
| | `POST` | `/ingest` | Upload a PDF; returns chunk count and IDs | | |
| | `POST` | `/query` | Ask a question; returns answer + sources + confidence | | |
| | `POST` | `/query/stream` | Same as `/query` but streams as SSE | | |
| | `POST` | `/debug/chunks` | Return raw retrieved chunks without calling the LLM | | |
| All `POST` query endpoints accept `{"question": "...", "top_k": 5}`. | |
| The `/debug/chunks` endpoint is useful for diagnosing retrieval issues β it shows each chunk's rank, cosine similarity score, source, page, section header, and a 300-character text preview. | |
| ## Evaluation | |
| The evaluation framework has three layers: | |
| **Retrieval metrics** (`evaluation/eval.py`) β given a test set of `(question, expected_source, expected_page)` tuples, measures whether the correct chunk appears in the top-k results: | |
| | Metric | Definition | | |
| |---|---| | |
| | Accuracy@K | Fraction of queries where correct chunk is in top K | | |
| | MRR | Mean Reciprocal Rank of the first correct result | | |
| **End-to-end metrics** (`evaluation/eval_e2e.py`) β runs the full pipeline and scores each answer: | |
| | Metric | Scorer | | |
| |---|---| | |
| | Faithfulness | LLM judge: are all claims grounded in retrieved context? | | |
| | Relevance | LLM judge: does the answer address the question? | | |
| | Keyword coverage | Fraction of expected answer keywords found in output | | |
| **Chunking strategy comparison** (`evaluation/compare.py`) β grid search over chunk sizes, overlap values, and re-ranking on/off. Results table (fill in with your benchmark numbers): | |
| | Strategy | Chunk size | Overlap | Reranking | Acc@5 | MRR | Faithfulness | Relevance | | |
| |---|---|---|---|---|---|---|---| | |
| | recursive_char | 300 | 0 | No | β | β | β | β | | |
| | recursive_char | 500 | 50 | No | β | β | β | β | | |
| | recursive_char | 500 | 50 | Yes | β | β | β | β | | |
| | recursive_char | 1000 | 100 | Yes | β | β | β | β | | |
| | semantic | 500 | β | Yes | β | β | β | β | | |
| ## Project Structure | |
| ``` | |
| rag-qa/ | |
| βββ main.py # FastAPI server | |
| βββ app_gradio.py # Gradio web UI | |
| βββ requirements.txt | |
| βββ Dockerfile | |
| βββ .env.example | |
| β | |
| βββ ingestion/ | |
| β βββ pdf_reader.py # pymupdf4llm + pdfplumber extraction | |
| β βββ chunker.py # fixed_size, recursive_char, semantic strategies | |
| β βββ embedder.py # SentenceTransformer + contextual enrichment | |
| β βββ pipeline.py # Orchestrates extract β chunk β embed β index | |
| β | |
| βββ retrieval/ | |
| β βββ index.py # FAISS IndexFlatIP wrapper | |
| β βββ bm25_index.py # BM25Okapi wrapper | |
| β βββ reranker.py # Cross-encoder re-ranking | |
| β βββ searcher.py # Hybrid search + expansion + confidence score | |
| β | |
| βββ generation/ | |
| β βββ generator.py # Groq client, confidence gating, streaming | |
| β | |
| βββ evaluation/ | |
| β βββ judge.py # LLM-as-judge (faithfulness + relevance) | |
| β βββ eval.py # Retrieval metrics: Acc@K, MRR | |
| β βββ eval_e2e.py # End-to-end pipeline evaluation | |
| β βββ compare.py # Configuration grid comparison | |
| β | |
| βββ data/ # Sample PDFs for testing | |
| ``` | |
| ## How It Works | |
| **PDF Extraction** β `pymupdf4llm` converts each page to structured markdown, preserving headings and table layout. If it fails, `pdfplumber` extracts flat text. Pages are processed independently so that `page_num` metadata stays accurate for citations. | |
| **Chunking** β three strategies are available. `fixed_size` is a naive sliding window (fast baseline). `recursive_character` uses LangChain's splitter with markdown-aware separators (`\n## `, `\n\n`, `. `) and extracts the nearest heading above each chunk as `section_header` metadata. `semantic` embeds individual sentences and splits at cosine similarity drops below a threshold, grouping topically coherent content together. | |
| **Embedding** β before embedding, each chunk's text is prefixed with `"<filename> | Page N [| Section]\n"`. This means the stored vector encodes not just the chunk's content but where in the document it came from, which improves retrieval precision for location-specific queries. | |
| **Retrieval** β a query vector is searched against FAISS (semantic) and BM25 (keyword) simultaneously. Both result sets are min-max normalized to `[0, 1]` and fused by weighted sum. The top 20 fused candidates are then re-scored by the cross-encoder, which reads the full `(query, chunk)` string pair and produces a relevance score independent of embedding geometry. | |
| **Generation** β `max_score` (cosine similarity of the top-ranked chunk before re-ranking) determines whether to call the LLM. Below 0.3, the call is skipped. Between 0.3 and 0.5, the answer is appended with a confidence warning. Above 0.5, the model receives a numbered context block and is prompted to cite every claim with `[Source N]` notation and synthesize across sources. | |
| ## Deployment | |
| **Docker:** | |
| ```bash | |
| docker build -t rag-qa . | |
| docker run -p 7860:7860 -e GROQ_API_KEY=your_key rag-qa | |
| ``` | |
| **HuggingFace Spaces:** | |
| Set `GROQ_API_KEY` in your Space's Settings β Repository secrets, then push the repository. The `Dockerfile` is picked up automatically by Spaces. | |
| ## Future Improvements | |
| - **OCR support** β integrate `pytesseract` or `surya-ocr` for scanned PDFs that contain no extractable text layer | |
| - **Persistent vector store** β replace the in-memory FAISS index with a disk-backed store (FAISS `write_index` with reload on startup, or a dedicated vector DB) so the index survives server restarts | |
| - **Multi-document cross-referencing** β surface when the same fact is corroborated across multiple uploaded documents rather than citing only one source | |
| - **GPU-accelerated embedding** β the sentence-transformers model runs on CPU by default; passing `device="cuda"` cuts batch embedding time significantly for large document sets | |