Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ask the Python Docs

A retrieval-augmented chatbot over Python's official documentation for asyncio and typing, with an admin dashboard that tracks every question asked, which documentation chunks were retrieved, and where retrieval struggles.

Built as a focused demo of production RAG patterns: structured ingestion, citation-grounded answers, observability, and input hardening — deployable end-to-end on free-tier hosting.

Try it live

Live demo: https://rag-python-doc.vercel.app

Try a question like:

How do I run multiple coroutines concurrently?

You'll get a streamed answer with clickable citations back to docs.python.org. The admin dashboard at /admin is password-gated and shows every question, the chunks retrieved, and any inputs flagged by the moderation + injection layers.

Hosted on Render's free tier — first request after a cold start may take ~30 seconds while the service wakes up.

Screenshots

Chat with citations Admin dashboard
Chat UI Admin dashboard

Architecture

                 ┌──────────────────────────────┐
                 │   React + TS + Vite (Vercel) │
                 │   Chat UI   Admin Dashboard  │
                 └──────────────┬───────────────┘
                                │  JSON / SSE
                                ▼
  ┌─────────────────────────────────────────────────────────┐
  │            FastAPI backend (Render)                     │
  │  ┌───────────────┐  ┌────────────────┐  ┌────────────┐  │
  │  │ /api/chat     │  │ /api/admin/*   │  │  session   │  │
  │  │ (SSE stream)  │  │ (cookie auth)  │  │  cookie    │  │
  │  └───────┬───────┘  └────────┬───────┘  └────────────┘  │
  │          │                   │                          │
  │  ┌───────▼──────────┐  ┌─────▼───────────────────────┐  │
  │  │ RAG pipeline     │  │ SQLite (aiosqlite)          │  │
  │  │  • moderation    │  │  • questions                │  │
  │  │  • injection     │  │  • retrieved_sources        │  │
  │  │    detection     │  │  • flagged_inputs           │  │
  │  │  • retrieval     │  └─────────────────────────────┘  │
  │  │  • OpenAI stream │                                   │
  │  └───────┬──────────┘                                   │
  └──────────┼──────────────────────────────────────────────┘
             ▼
      ChromaDB (on-disk)
      python_docs collection
      • api-entry chunks (one per dl.py.function/class/method)
      • prose chunks at section boundaries
      • metadata: source_url, heading_path, module, anchor

Stack

  • Backend: Python 3.11, FastAPI, SQLAlchemy 2 (async), aiosqlite, LlamaIndex, OpenAI SDK
  • Vector store: ChromaDB, on-disk, persisted at backend/data/chroma/
  • Frontend: React + TypeScript (strict) + Vite + Tailwind
  • Docs source: official Python 3.12 HTML archive, pinned to a patch version

Local setup

# 1. Clone and enter
git clone <repo> && cd python-doc-chat

# 2. Backend (requires Python 3.11+ — install via pyenv/asdf if needed)
cd backend
python3.11 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp ../.env.example ../.env          # then fill in OPENAI_API_KEY and ADMIN_PASSWORD

# 3. Ingest Python docs (one-time, ~2 min; embeds ~400 chunks)
python scripts/ingest.py

# 4. Seed the admin dashboard from the captured fixture
python scripts/seed_db.py

# 5. Run the API
uvicorn app.main:app --reload

# 6. Frontend (in a second terminal)
cd ../frontend
npm install && npm run dev

Interactive API docs auto-generated by FastAPI at http://localhost:8000/docs.

Retrieval evaluation

This project ships with a first-class retrieval eval harness rather than a scratch script. backend/scripts/eval.py runs a golden set of five queries against the live Chroma index and asserts that expected documentation anchors appear within a target top-k rank.

cd backend && python scripts/eval.py              # print results to stdout
cd backend && python scripts/eval.py --write-md   # regenerate EVAL_RESULTS.md

Exit code is non-zero on unexpected regressions. Failures that match a documented weakness are marked KNOWN FAIL and don't fail the suite — if a tuning change flips one to IMPROVED, that's the signal to update the file.

Current baseline: EVAL_RESULTS.md3/5 pass, 2 known failures documented honestly. The known failures inform the What I'd do differently at scale section.

Prompt-injection hardening

Three layers, in order of enforcement strength:

  1. OpenAI Moderation API on every user input. Flagged inputs return a canned refusal and are logged with their flagged categories. This does block.
  2. Regex pattern detection for known prompt-injection shapes (role-swap attempts, system-prompt leaks, "ignore previous instructions" variants). Matches are logged to flagged_inputs but do not block — they exist as observability signal for the admin dashboard.
  3. Scoped system prompt + Pydantic length cap. The model is told explicitly to answer only from retrieved context and to refuse off-topic questions. Input is capped at 1000 characters at the schema layer.

What this is, honestly: observability-grade, not security-grade. A determined adversary will get past the regex list. The point of this layer is to give the admin dashboard a meaningful signal about what people try, not to make the system bulletproof.

What a production version would add: a trained classifier for injection scoring (not regex), output moderation on the generated response (not just input), a secondary LLM pass to verify the response stays on-topic, and rate limits tied to session identity rather than IP.

Admin dashboard

Password-protected via a single ADMIN_PASSWORD env var. Login POSTs the password, backend issues an HTTP-only signed cookie (itsdangerous), subsequent requests gated by a FastAPI Depends().

Dashboard shows:

  • All questions with timestamp, session ID, retrieved sources, generated answer
  • Questions-per-day chart (last 14 days)
  • Most-retrieved doc sections
  • Mean retrieval similarity per question
  • Flagged inputs table (moderation + regex) with filters

Seed data is real, not synthetic

scripts/capture_seed.py runs each demo prompt once through the actual chat pipeline (moderation → retrieval → OpenAI → flag detection) and saves the result to data/seed_fixture.json. scripts/seed_db.py loads that fixture on startup or on demand. The dashboard shows actual model output, real retrieval scores, and a real moderation block — captured once, committed to the repo, replayed deterministically. Costs ~$0.03 in OpenAI credit to recapture; the fixture is offline + free to seed thereafter.

Deployment

  • Backend → Render (free web service). ChromaDB index is committed to the repo because Render's free tier has no persistent disk. Rebuilds happen by running scripts/ingest.py --rebuild locally and committing the result.
  • Frontend → Vercel (free static hosting).

What I'd do differently at scale

Fix the two known retrieval failures

The eval harness (EVAL_RESULTS.md) surfaces two specific weaknesses in the baseline cosine-similarity retriever:

  • Q1 — prose chunks outrank terse API signatures on natural-language queries. "How do I run multiple coroutines concurrently?" returns generic coroutine prose instead of asyncio.gather / asyncio.TaskGroup, even though those chunks exist and rank first on literal-name queries. Fix: enrich each API chunk's embedding text with a natural-language intro derived from the first paragraph of its <dd> description, so a how-to question matches the API entry's semantic content rather than just its signature.
  • Q5 — term ambiguity + module imbalance. "What is a Protocol?" returns only asyncio.*Protocol chunks because asyncio contributes 8+ Protocol- related entries while typing contributes exactly one. Cosine similarity has no mechanism to enforce module diversity. Fix: apply MMR (Maximal Marginal Relevance) re-ranking to diversify top-k across modules — retrieve top-20, then greedily pick 5 that balance relevance with inter-result dissimilarity.
  • Broader mitigation for both: hybrid BM25 + vector retrieval with reciprocal rank fusion. Lexical matching gives literal terms (Protocol, gather) a floor when the embedding model under-weights them, and the fusion score tends to smooth over the prose-vs-API mismatch from Q1.

Beyond retrieval

  • ChromaDB → managed vector DB (Pinecone, pgvector, or Turbopuffer). A single-node embedded store is fine up to ~10⁵ chunks; beyond that it bottlenecks on ingest, replication, and cross-region latency.
  • SQLite → managed Postgres with proper migrations (Alembic). SQLite is here to keep the demo free-tier and reviewable; it's not the right pick above a handful of concurrent writers.
  • Ingest as a pipeline, not a script. Version each chunk by (doc_version, source_url, content_hash), support incremental re-embedding on upstream docs changes, and run it on a schedule with dbt-style freshness checks.
  • Expand the eval. Grow the golden set from 5 to ~50 queries drawn from real admin-dashboard traffic; add Recall@k and NDCG metrics; gate CI on it; track scores over time so tuning changes have a visible regression signal.
  • Output-side safety. Moderate the generated response, not just input; add a secondary model to verify factual grounding in the retrieved context.
  • Auth. Swap the single-password admin for SSO (Clerk, Auth0, WorkOS) and add per-user rate limits on the chat endpoint.
  • Observability. Ship structured logs + traces to Honeycomb/Datadog; track retrieval latency, LLM latency, and token cost as first-class metrics.

Project layout

backend/
  app/
    main.py              FastAPI entry, router mounts, CORS, lifespan
    core/                config (pydantic-settings), shared dependencies
    routers/             chat.py, admin.py — one APIRouter per feature
    rag/                 parser, chunker, indexer, retriever, prompts
    db/                  SQLAlchemy async models + session
    security/            injection patterns, moderation, admin auth dep
    schemas/             Pydantic request/response models
  data/
    raw/                 downloaded HTML (gitignored)
    chroma/              vector store (committed — see Deployment above)
    app.db               SQLite runtime DB (gitignored)
  scripts/
    ingest.py            one-shot: download → chunk → embed → index
    eval.py              golden-query retrieval eval → EVAL_RESULTS.md
    capture_seed.py      one-time: run prompts through real pipeline, snapshot to JSON
    seed_db.py           load data/seed_fixture.json into the admin DB

frontend/
  src/
    pages/               Chat.tsx, AdminLogin.tsx, AdminDashboard.tsx
    components/          reusable UI
    lib/                 typed API client

License

MIT.

Releases

Packages

Contributors

Languages