Upload PDFs, ask questions, get answers with citations that are checked against the source text before you ever see them - and an honest "not found" when your documents do not contain the answer.
▶ Watch the 25-second demo video | Try the live demo
Teams sit on piles of PDFs - policy manuals, product manuals, contracts - and answering "what does the document actually say?" means scrolling and guessing. Generic chatbots answer confidently even when the document says nothing, and their "citations" often point at text that does not exist. A document assistant is only useful if every answer can be traced to a real passage, and if "we don't know" is a first-class answer.
- Upload PDFs; each is extracted per page, split into heading-aware chunks and indexed in Postgres (pgvector + full text).
- Ask questions in a chat UI; retrieval is hybrid (vector + keyword, reciprocal-rank fusion), generation goes through a Gemini -> Groq fallback router with a strict Pydantic schema.
- Every citation is verified deterministically: the quote must appear verbatim (whitespace-normalized) in a retrieved chunk from the same doc and page. Bad citations are dropped, never shown.
- If nothing verifiable remains, you get "Not found in your documents" plus the three closest passages - never an invented answer.
- Built-in eval harness (
make eval,GET /eval) reports retrieval recall@8 and citation accuracy over a 20-question gold set. - Re-uploading the same file is a no-op (idempotency keys), and every pipeline stage writes an audit row.
▶ Watch the 25-second demo video | Try the live demo
flowchart LR
UI[static UI<br>index.html + app.js] --> API[FastAPI<br>create_app: /health, rate limit]
API --> ING[ingest.py<br>pypdf + heading chunker]
API --> ANS[answer.py<br>retrieve -> generate -> verify]
ING --> VS[(Postgres 16 + pgvector<br>chunks / documents / questions<br>audit_log / processed_keys)]
ANS --> VS
ANS --> R[aiforge-core Router<br>prompts/answer.v1.md]
R --> G1[Gemini 2.5 Flash]
R --> G2[Groq llama-3.3-70b]
ANS --> V[verify.py<br>quote must exist in chunk<br>same doc + page]
EV[eval.py<br>gold.jsonl, recall@8,<br>citation accuracy] --> VS
All model calls go through aiforge_core.llm.Router.generate_json with the Answer
Pydantic schema; embeddings are local MiniLM (384-dim) or a deterministic stub.
- Citation verification: every quote must appear as a whitespace-normalized substring of a retrieved chunk with the same doc and page. Anything else is dropped.
- Honest not-found: if the model says
found=false, or all citations are dropped, the status isnot_foundand the UI shows the three closest passages instead of an answer. - Schema-validated output: the model must return valid
AnswerJSON; a broken response raisesValidationFailedand becomes an honest failure, never partial data. - Idempotent ingest:
once(key("ingest", sha256(bytes)))- re-uploading the same PDF creates no duplicate document or chunks. - Audit trail: one
audit_logrow per stage (ingest,retrieve,generate,verify) with prompt version and input hash. - Rate limit: per-IP sliding window (default 30/min) protects free LLM quotas.
- PDFs only, text-based (no OCR for scanned pages, no tables/figures understanding).
- English-tuned retrieval (Postgres
englishtext search config, MiniLM embeddings). - Chunk sizes use a word-count approximation of tokens (words x 1.3), not a real tokenizer - documented, deterministic, close enough for ~500-token chunks.
- One answer prompt, no conversation memory: each question stands alone.
- No auth/multi-tenancy: one shared document space per deployment.
LLM_PROVIDER_ORDER=mock(keyless demo) serves canned answers matched to the question: the 20 gold questions (plus "What is the return window?") get their cited answer, everything else an honest not-found. No key, no model, still fully verified against the real chunks.
make setup # venv + deps, start postgres (docker), run migrations
make demo # seed the 3 sample PDFs, serve http://localhost:8000
make eval # print recall@8 and citation accuracyCopy .env.example to .env and set GEMINI_API_KEY (and optionally GROQ_API_KEY).
For a fully offline, keyless demo: EMBEDDER=stub LLM_PROVIDER_ORDER=mock make demo
(no model downloads, canned answers). make demo with real embeddings needs the
demo extra: uv pip install -p .venv -e ".[demo]" (pulls sentence-transformers).
- Neon: create a free project, enable the
vectorextension, copyDATABASE_URL. - Render: new Web Service from this repo, Docker runtime, add the env vars from
.env.example, health check path/health. - First boot runs migrations and seeds the sample PDFs automatically (idempotent).
- Confirm
https://<app>.onrender.com/healthreturns{"ok": true}. - Rate limit stays on. Free Gemini quota is enough for demo traffic.
api/index.py + vercel.json expose the app on Vercel's Python runtime;
requirements.txt holds the runtime-only deps. Set DATABASE_URL (Neon pooled
Postgres with pgvector), LLM_PROVIDER_ORDER=mock, EMBEDDER=stub. Cold starts
run the idempotent migrations only; seed the demo database once from any machine:
set DATABASE_URL=postgresql://<user>:<pass>@<host>/<db>
.venv\Scripts\python.exe -m scripts.seed_remote
(idempotent: migrations + ingest of the three sample PDFs with the stub embedder,
so /ask and /eval work immediately; safe to re-run).
make test - offline, no keys, MockProvider + StubEmbedder (DB tests use the
throwaway-database fixture from aiforge-core):
test_chunking- a heading lands in the same chunk as its first paragraph, chunk overlap is present, no empty chunks.test_citation_verify- a quote not present in the retrieved text is dropped; when every citation is dropped the result isnot_found.test_not_found-found=falsefrom the model yieldsnot_foundwith exactly 3 closest passages, a stored question row and audit rows for all stages.test_hybrid_merge- vector and keyword results sharing a chunk id fuse into one entry (reciprocal-rank fusion, deduped).test_ingest_once- the same bytes uploaded twice create one document and no duplicate chunks.test_eval_offline- the eval harness runs fully offline and prints both metrics.
Eval numbers on the committed gold set (20 questions, StubEmbedder + canned answers, fully offline):
| metric | value |
|---|---|
| recall@8 | 1.00 |
| citation accuracy | 1.00 |
RAG, retrieval augmented generation, document AI, PDF chatbot, knowledge base assistant, pgvector, hybrid search, verified citations, hallucination prevention, LLM evaluation, FastAPI, PostgreSQL.



