AskTheHandbook / README.md
rishitpant's picture
Update README
44179ec
|
Raw
History Blame Contribute Delete
10.6 kB
metadata
title: AskTheHandbook
emoji: πŸŽ“
colorFrom: blue
colorTo: indigo
sdk: docker
pinned: false
app_port: 7860

AskTheHandbook

A production-style RAG system that answers student questions about the IITM BS Degree Programme (fees, grading, exams, eligibility, academic structure) strictly grounded in the official handbook and grading documents, with source citations.

πŸ”— Live demo: huggingface.co/spaces/rishitpant/AskTheHandbook

Features:

  • Hybrid retrieval + cross-encoder reranking
  • Versioned prompts
  • Automated evaluation harness with checkpointing
  • Langfuse observability
  • Rate limiting via slowapi (5 requests/minute per IP, proxy-aware)
  • CI quality gate with auto-deploy to Hugging Face Spaces
  • FastAPI backend + web chat UI, Dockerized for production

Architecture

source PDFs  β†’  LlamaParse  β†’  data/*.md  β†’  ingest.py  β†’  Chroma Cloud (or local ChromaDB)
                                                                  β”‚
question  β†’  retrieve.py  ────────────────────────────────────────┴─── BM25 (keyword index)
                  β”‚
          EnsembleRetriever (RRF fusion, weights 0.6/0.4)
                  β”‚
          Cross-encoder reranker (ms-marco-MiniLM-L-6-v2)
                  β”‚
          top-N chunks (with source + section metadata)
                  β”‚
generate.py  β†’  prompts.yaml (versioned prompts)
                  β”‚
          Groq LLM (llama-3.3-70b-versatile), streamed
                  β”‚
          Grounded answer + cited sources
                  β”‚
   app.py (FastAPI) β†’ static/index.html (chat UI) / CLI
                  β”‚
          Langfuse trace (latency, chunk metadata, rerank scores)

Project layout

RAG/
β”œβ”€β”€ app.py                         # FastAPI app (chat/stream + static UI + rate limiting)
β”œβ”€β”€ static/
β”‚   └── index.html                 # chat frontend
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ student-handbook.md        # source document 1
β”‚   └── grading-document.md        # source document 2
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ ingest.py                  # chunk + embed + index (local or Chroma Cloud)
β”‚   β”œβ”€β”€ retrieve.py                # hybrid search + rerank
β”‚   β”œβ”€β”€ generate.py                # Generator class, RAG chain + CLI chat
β”‚   └── tracing.py                 # Langfuse observability (opt-in)
β”œβ”€β”€ eval/
β”‚   β”œβ”€β”€ eval_prompts.json          # 10-question set
β”‚   β”œβ”€β”€ evaluate.py                # DeepEval harness
β”‚   └── report.json                # latest run's results
β”œβ”€β”€ db/                            # local ChromaDB vector store (generated)
β”œβ”€β”€ prompts.yaml                   # versioned prompts (prod + eval)
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ requirements.txt               # full dev environment
β”œβ”€β”€ requirements-prod.txt          # slim deps for Docker/CI
└── .github/workflows/ci.yaml      # eval gate + HF Spaces deploy

Ingestion (src/ingest.py)

  • Source docs: PDFs (exported from Google Docs) converted to Markdown via LlamaParse (student-handbook.md, grading-document.md)
  • Cleans residual Google Docs export noise (page numbers, "Published using Google Docs" footers, info-icon artifacts) carried over from the source PDFs; converts HTML tables to Markdown tables
  • Header-aware splitting (MarkdownHeaderTextSplitter) + RecursiveCharacterTextSplitter (2000 char chunks, 200 overlap)
  • Wraps each chunk with [Course: h1 > h2 > h3] tags for both vector and BM25 search
  • Embeddings: BAAI/bge-small-en-v1.5
  • Writes to Chroma Cloud if CHROMA_API_KEY is set, else to local ChromaDB

Retrieval (src/retrieve.py)

  • Hybrid search: vector + BM25, fused via EnsembleRetriever (weights [0.6, 0.4]), 8 candidates each
  • Cross-encoder reranking: cross-encoder/ms-marco-MiniLM-L-6-v2 rescores every (query, chunk) pair
  • ToC penalty: deprioritizes table-of-contents-like chunks
  • Scrubs residual export noise before chunks reach the LLM
  • Returns top N chunks (default 5) with text, source, page (section path), rerank_score
  • Same Chroma Cloud / local switch as ingestion, via USE_CHROMA_CLOUD

Generation (src/generate.py)

  • Generator class wraps the retriever + Groq LLM client, used by both the CLI and the API
  • Prompts loaded from prompts.yaml (not hardcoded), version printed at startup
  • System prompt enforces: strict grounding, no hallucination, table extraction, mandatory citations, defining acronyms, flagging overlapping fee figures, one course at a time
  • Streams tokens via Groq's OpenAI-compatible endpoint
  • Model overridable via RAG_MODEL env var (default: llama-3.3-70b-versatile)
  • get_sources() returns the deduplicated source list for the last answer
  • Each turn wrapped in an @observe span for tracing

Web app (app.py, static/index.html)

  • FastAPI app exposing:
    • GET /api/health β€” pipeline/backend status
    • POST /api/chat β€” non-streaming chat
    • POST /api/chat/stream β€” Server-Sent Events streaming chat
  • RAG pipeline loaded once at startup (FastAPI lifespan), not per-request
  • Static chat UI served from static/ at /
  • Rate limiting: 5 requests/minute per IP via slowapi; proxy-aware (reads X-Forwarded-For)
  • CORS open by default (allow_origins=["*"])

Observability (src/tracing.py)

  • Langfuse tracing is opt-in via LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST
  • When enabled: each turn produces a trace with a hybrid_retrieval span (chunks, sections, rerank scores, model costs, latency)
  • When disabled: observe becomes a no-op β€” no tracing dependency required

Evaluation (eval/evaluate.py)

  • 20-question set (eval/eval_prompts.json), curated for category coverage, not size due to api rate limits
  • Weak-scoring questions are kept deliberately to surface gaps, not flatter the score

Pipeline per question:

  • Retrieve top-4 chunks via the full hybrid + rerank pipeline
  • Generate an answer with llama-3.3-70b-versatile (eval_system + shared human prompt, 150-token cap)
  • Fast keyword-hit check (no LLM) due to api rate limits
  • Three DeepEval LLM-judge metrics (same model as judge): Faithfulness, Answer Relevancy, Contextual Precision.

Reliability features:

  • Auto-retry with backoff on Groq 429s
  • Throttling between calls/metrics to stay under rate limits
  • Per-question checkpointing (eval_checkpoint.json) β€” resumes after a crash, clears on a clean pass
  • Every run writes eval/report.json: model/prompt versions, per-metric averages, per-question breakdown, pass/fail gate
  • Judge model overridable via JUDGE_MODEL env var (default: llama-3.3-70b-versatile); CI uses llama-3.1-8b-instant for speed

Latest result (20 questions, judge = llama-3.3-70b-versatile, threshold 0.8):

Metric Score
Keyword Hit Rate 100%
Faithfulness (avg) 91%
Answer Relevancy (avg) 91%
Contextual Precision (avg) 96%
Gate (min of keyword & faithfulness) 0.908 β€” PASSED
  • Judge choice matters: an 8B judge produced a gate score of 0.618 with false-zero faithfulness scores on verified-correct answers β€” it lacks reasoning capacity for compound eligibility/fee questions. The 70B judge raised the gate to 0.908, reflecting actual system quality. CI keeps the 8B model for speed; 70B is used for local runs.

Prompt versioning (prompts.yaml)

  • system β€” full production system prompt (generate.py)
  • eval_system β€” leaner version for the 150-token eval budget, same core grounding/refusal behavior
  • human β€” shared {context} / {question} template for both prod and eval
  • version β€” bumped on any prompt change, recorded in every report.json

Current version: 1.4.0


CI/CD (.github/workflows/ci.yaml)

On push to main:

  1. Quality gate β€” installs requirements-prod.txt, runs the keyword-only eval against Chroma Cloud using llama-3.1-8b-instant (python eval/evaluate.py --no-deepeval --threshold 0.8)
  2. Deploy β€” if the gate passes, force-pushes the repo to the linked Hugging Face Space, which rebuilds the Docker image and redeploys
  • DeepEval (faithfulness/relevancy/precision) gate is not yet wired into CI due to api rate limits β€” roadmap item, planned for PRs into main only (~5–10 min throttled runtime)

Deployment

  • Docker: Dockerfile builds a slim image (requirements-prod.txt), runs uvicorn app:app on port 7860
  • Hugging Face Spaces: repo includes HF Spaces metadata (top of this file); CI auto-deploys on every successful main push
  • Vector store in prod: the image does not ship data/ or a local db/ β€” it expects CHROMA_API_KEY/CHROMA_TENANT/CHROMA_DATABASE pointing at a pre-ingested Chroma Cloud collection

How to run

pip install -r requirements.txt

# Build the vector index (required before first run)
python src/ingest.py

# Chat with the assistant (CLI)
python src/generate.py

# Run the web app locally
uvicorn app:app --reload
# β†’ open http://localhost:8000

# Run the full eval suite (DeepEval + keyword gate)
python eval/evaluate.py --save-report

# Run keyword-only gate (fast, no LLM judge)
python eval/evaluate.py --no-deepeval --threshold 0.5
# Or run via Docker
docker build -t askthehandbook .
docker run -p 7860:7860 --env-file .env askthehandbook

Environment variables (create a .env file):

GROQ_API_KEY=your_key_here

# Optional β€” use Chroma Cloud instead of local persistent ChromaDB
CHROMA_API_KEY=...
CHROMA_TENANT=...
CHROMA_DATABASE=...

# Optional β€” enables Langfuse tracing
LANGFUSE_PUBLIC_KEY=...
LANGFUSE_SECRET_KEY=...
LANGFUSE_HOST=https://cloud.langfuse.com

Tech stack

LangChain (LCEL) Β· ChromaDB / Chroma Cloud Β· BM25 (rank-bm25) Β· BAAI/bge-small-en-v1.5 embeddings Β· cross-encoder/ms-marco-MiniLM-L-6-v2 reranker Β· Groq (llama-3.3-70b-versatile) Β· FastAPI Β· slowapi Β· DeepEval Β· Langfuse Β· GitHub Actions Β· Docker Β· Hugging Face Spaces


What's next

  • Wire the DeepEval gate into CI (currently keyword-only; scope to PRs into main)
  • Improve Contextual Precision on scheduling/fee-overlap questions β€” tune BM25/vector weights, increase HYBRID_TOP_K, or add query expansion
  • Add auth to the public API endpoints before wider sharing