A production-grade RAG starter that answers from your sources, cites them, and says "I don't know" instead of hallucinating.
Most RAG demos look great until real users hit them — then they hallucinate, double-charge on retries, re-embed everything on every deploy, and you have no way to tell if a prompt change made things worse. Grounded is the boring, reliable parts done right, in a small codebase you can read in 20 minutes and ship on.
- ✅ Cited answers — every answer references the source chunks it used.
- ✅ "I don't know" guardrail — if nothing relevant is retrieved, it refuses instead of guessing.
- ✅ Idempotent ingestion — only new/changed chunks get embedded (content-hash dedup); re-ingesting is a near-no-op. No wasted API spend.
- ✅ Retries with backoff — transient API errors retried; 4xx fail fast.
- ✅ Eval harness — a labelled Q&A set scores retrieval + answers, so you catch regressions in CI.
- ✅ Pluggable + offline-testable — swap embedder/store/LLM via env; the default runs with no API key and no database (great for demos, tests, and CI).
Stack: TypeScript · Fastify · Postgres + pgvector · OpenAI (swappable).
npm install
npm start # offline mode: in-memory store + extractive answers# ingest some docs
curl -XPOST localhost:3000/ingest -H 'content-type: application/json' \
-d '{"docs":[{"id":"faq","source":"faq.md","text":"Refunds are allowed within 30 days."}]}'
# ask — grounded answer with citations
curl -XPOST localhost:3000/ask -H 'content-type: application/json' \
-d '{"question":"What is the refund window?"}'
# ask something off-topic — it refuses instead of making things up
curl -XPOST localhost:3000/ask -H 'content-type: application/json' \
-d '{"question":"How do I train a neural net?"}' # → grounded:falseRun the eval + tests:
npm run eval # scores the bundled Q&A set (offline)
npm test # 10 tests, no API key / DB neededcp .env.example .env
# set PROVIDER=openai, STORE=pgvector, OPENAI_API_KEY=..., DATABASE_URL=...
docker compose up -d # Postgres with pgvector
npm run migrate # create the vector table + cosine index
npm run ingest -- ./your-docs # embed your corpus (idempotent)
npm startWorks with Azure OpenAI / proxies via OPENAI_BASE_URL.
Idempotent ingestion (src/core/pipeline.ts): each chunk is keyed by a sha256 of its text. On ingest we embed only chunks whose hash isn't already stored, and prune chunks that no longer exist. Change one paragraph → only that chunk re-embeds.
The guardrail (ask()): we retrieve top-k, and if the best cosine similarity is below RAG_MIN_SCORE, we return a refusal — the model is never even called, so it can't hallucinate (and you don't pay for it).
Citations: the answer ships with the exact source chunks + scores it was built from, so users (and you) can verify it.
Evals (src/eval/run.ts): a JSON dataset of questions with expected sources / grounded-ness / answer substrings. npm run eval scores retrieval hit-rate and answer correctness — run it in CI to catch regressions from a prompt/model/chunking change.
ingest/ask → Embedder × VectorStore × Chat
├ OpenAI (prod) ├ pgvector (prod) ├ OpenAI (prod)
└ Hash (offline) └ Memory (offline) └ Extractive (offline)
Same pipeline code regardless — only the adapters change. That's why the whole thing is testable offline.
| Method | Path | |
|---|---|---|
| POST | /ingest |
{ docs: [{id, source?, text}] } → ingest summary |
| POST | /ask |
{ question, k?, minScore? } → { answer, citations, grounded } |
| GET | /stats |
chunk count + active providers |
| GET | /health |
liveness |
Built by Exavel — we build AI features that hold up in production. If your AI feature is flaky, hallucinating, or expensive, that's what we do. (See also Holdfast, an oversell-proof, chaos-tested reservation engine.)
MIT licensed — use it, fork it, ship it.