Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DOCIQ — RAG Document Intelligence

CI

Upload documents, ask questions, get answers grounded in what you uploaded — with the retrieved passages and their similarity scores shown next to every answer, so you can check the model's work.

React · FastAPI · ChromaDB · sentence-transformers · Claude


How it works

Browser                    FastAPI backend                     Claude
   │                             │                                │
   ├── POST /upload ────────────▶│ extract text (pypdf for PDFs)  │
   │                             │ chunk: 400 words, 20% overlap  │
   │                             │ embed: all-MiniLM-L6-v2        │
   │                             │ store: ChromaDB (cosine)       │
   │                             │                                │
   ├── POST /query ─────────────▶│ embed query                    │
   │                             │ retrieve top-3 by similarity   │
   │                             │ build grounded prompt ────────▶│
   │◀── answer + sources + ──────│◀───────────────────────────────┤
        retrieved chunks

The browser never calls the Claude API directly — the API key stays on the server. Retrieval is semantic (embedding similarity), not keyword matching, so a question phrased in different words than the source still finds the right passage.

Retrieval evaluation

Retrieval is the half of RAG that decides whether a correct answer is even possible: if the right chunk never reaches the model, no prompt recovers it. So it's measured on its own, against a golden set of questions deliberately worded to share no vocabulary with their answers.

python eval/eval_retrieval.py
DOCIQ retrieval eval: 10 questions over 9 chunks

  [PASS] How do plants of the electrical kind turn daylight into po rank 1
  [PASS] At what point does a turbine stop spinning to protect itse rank 1
  [near] Which technology handles the evening demand spike once the rank 2
  [PASS] What stores energy by lifting water to a higher elevation? rank 1
  [MISS] Which renewable source keeps producing regardless of the w not in top 3
  [PASS] How much does panel output fall off each year?             rank 1
  [PASS] Why is putting turbines out at sea more productive?        rank 1
  [PASS] How many charge cycles before capacity drops to eighty per rank 1
  [PASS] What fraction of energy survives a round trip through the  rank 1
  [PASS] What makes deep underground heat expensive to exploit?     rank 1

  hit@1 80%    hit@3 90%    MRR 0.850

hit@1 is how often the top result is correct, hit@3 whether it reaches the model at all, and MRR the mean reciprocal rank. The script exits non-zero below a configurable hit@3 threshold, so a regression in chunking or embedding fails CI instead of quietly degrading answers.

The one miss is instructive and left in deliberately. "Which renewable source keeps producing regardless of the weather?" should match the geothermal passage, which says output is "continuous and unaffected by weather" — but the passage names wind and solar explicitly, so the embedding pulls toward those documents instead. That's a chunking problem, not a model problem: the distinguishing claim and the distracting comparison sit in the same chunk. It's exactly the kind of failure the eval exists to surface.

Tune and re-measure:

python eval/eval_retrieval.py --chunk-size 100 --overlap 10 --top-k 5

Deploy

render.yaml defines both services as a Render Blueprint — connect the repo on Render, and it provisions the FastAPI backend and the static frontend build together. The only manual step is entering ANTHROPIC_API_KEY when prompted; it's deliberately excluded from the blueprint so it's never committed.

Free-tier web services have an ephemeral filesystem — indexed documents reset after 15 minutes idle or on redeploy. That's expected for a public demo where each visitor uploads their own file in one sitting; a paid plan with an attached disk removes the limit if persistence across idle periods matters.

Setup

Requires Python 3.11+, Node 20+, and an Anthropic API key.

cp .env.example .env      # then add your key to .env

Docker

docker compose up --build

Frontend at http://localhost:5173, API at http://localhost:8000.

Manual

# Backend
cd backend
python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000

# Frontend (separate terminal)
cd frontend
npm install
npm run dev

The first backend start downloads the embedding model (~90 MB).

API

Interactive docs at http://localhost:8000/docs.

Method Endpoint Purpose
GET /health Liveness plus indexed-document count
POST /upload Ingest a .txt, .md, or .pdf (max 10 MB)
GET /documents List indexed documents
DELETE /documents/{doc_id} Remove a document and its chunks
POST /query Ask a question; optional doc_ids filter
curl -F "file=@notes.pdf" http://localhost:8000/upload

curl -X POST http://localhost:8000/query \
  -H "Content-Type: application/json" \
  -d '{"query": "What were the Q3 findings?"}'
{
  "answer": "Revenue grew 12% year over year, driven by...",
  "sources": ["q3-report.pdf"],
  "retrieved_chunks": [
    { "chunk": "Quarterly revenue grew by twelve...", "doc_name": "q3-report.pdf", "similarity": 0.87 }
  ]
}

Tests

cd backend && pytest

Chunking and text extraction are tested in isolation. The API tests run real retrieval against a temporary ChromaDB store with only the Claude call stubbed, so retrieval behaviour is genuinely exercised rather than mocked away.

Configuration

Variable Default Purpose
ANTHROPIC_API_KEY Required; the server won't start without it
CHROMA_DIR ./chroma_data Where vectors persist
CORS_ORIGINS http://localhost:5173,http://localhost:3000 Comma-separated allowed origins
VITE_API_BASE http://localhost:8000 Backend URL the frontend calls

Design notes

Chunk size — 400 words, 20% overlap. Large enough to keep a complete idea intact, small enough that a retrieved chunk is mostly signal. The overlap stops a sentence spanning a boundary from being lost to both neighbours.

Cosine over Euclidean distance. Sentence embeddings encode meaning in direction rather than magnitude, so angle is the metric that matters.

Thinking disabled on the generation call. Answering from supplied context is extraction, not reasoning — thinking tokens would add latency and cost without improving grounding.

Persistent Chroma, registry rebuilt on boot. Documents survive restarts; the in-process registry is reconstructed from stored metadata rather than being treated as the source of truth.

Limitations

  • Scanned/image-only PDFs are rejected rather than OCR'd.
  • No authentication — intended for local and demo use, not multi-tenant deployment.
  • The golden set is small (10 questions); it catches regressions, it doesn't establish absolute quality.

Possible extensions

  • Hybrid retrieval (BM25 + dense) and query rewriting
  • Streaming responses via SSE
  • Faithfulness scoring on the generation step, not just retrieval

License

MIT

About

RAG document Q&A — FastAPI + ChromaDB + Claude, with a retrieval eval harness gating CI (hit@3: 90%)

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages