groundrag
A production-grade advanced RAG system in Python, usable from a CLI, an HTTP API or as an MCP server (protocol revision 2026-07-28): multi-format ingestion, structure-aware chunking, hybrid dense + BM25 retrieval on LanceDB, query rewriting / multi-query / HyDE, cross-encoder reranking, cited generation with an explicit "not found" fallback, and an evaluation gate (recall@k, MRR, nDCG, faithfulness, answer relevance, abstention).
Docs: usage guide · configuration reference · design and default rationale.
Installation
pip install groundrag
# with extras (combine as needed):
pip install "groundrag[parsers,api,mcp,tiktoken,translation]"
The GitHub repository stays private until December 2026: until then the git form below and the repository links in this README work only for collaborators.
Or straight from GitHub:
pip install "git+https://github.com/andrea9293/groundrag.git"
pip install "groundrag[parsers,api,mcp,tiktoken,translation] @ git+https://github.com/andrea9293/groundrag.git"
| Extra | Enables |
|---|---|
[parsers] |
layout-aware parsing and image description (Docling + MarkItDown, ingest.parser: hybrid) |
[api] |
HTTP API (groundrag serve) |
[mcp] |
MCP server (groundrag mcp) |
[tiktoken] |
exact token counts (tokens.counter: tiktoken) |
[translation] |
translation to English (lingua, CTranslate2, transformers, sentencepiece, huggingface-hub); switches on translation.enabled: auto. The first model conversion also needs torch |
Development install:
git clone https://github.com/andrea9293/groundrag.git && cd groundrag
make venv install # python3 -m venv .venv && pip install -e ".[dev]"
The repository's config.yaml is picked up from the current directory. To keep personal settings
(endpoint, model, paths) out of git, copy it to a gitignored config.local.yaml and run
GROUNDRAG_CONFIG=config.local.yaml groundrag ... (or --config config.local.yaml).
Quickstart
The make targets below need a clone of the repository (they use data/sample/, config.yaml
and the Makefile). If you installed from PyPI, skip to the groundrag ... commands in the next
block.
make ingest # index data/sample (16 file types) into collection "default"
make query Q="What is the warranty period of the RX-200?"
make demo # ingest + questions answered from pdf, docx, xlsx, rst (+ a "not found")
make test # pytest (offline, deterministic stand-ins)
make test-real # opt-in: real embedding model, cross-encoder, LLM (network)
make eval # gold set → metrics → reports/eval_latest.json; non-zero exit on regression
make serve # HTTP API (pip install -e ".[api]")
make mcp # MCP server, stdio (pip install -e ".[mcp]") — see docs/USAGE.md §8
For layout-aware parsing and image description (hybrid Docling + MarkItDown): pip install -e ".[parsers]"
(see docs/USAGE.md §5).
With an OpenAI-compatible LLM: export OPENAI_API_KEY=... (or set llm.base_url for
Ollama / vLLM). Without one, the pipeline runs fully offline (see Limitations).
Quick start commands (your own documents; these work with pip install groundrag):
# 1. index your documents into a collection (incremental: unchanged files are skipped)
groundrag ingest docs/ --collection handbook
# 2. ask a question, offline: no LLM, the answer quotes the best sentences
groundrag --set llm.backend=none query "How many vacation days do I get?" --collection handbook
# 3. ask with an LLM (cited answer, refuses when the documents don't say)
# the key comes from the environment (never commit it); --set llm.base_url=... / llm.model=... pick another endpoint
export OPENAI_API_KEY=...
groundrag query "How many vacation days do I get?" --collection handbook
# 4. create a gold set with an LLM, review it, then set the collection's threshold
groundrag gold-generate --collection handbook --out data/eval/gold_handbook.jsonl
# dry run (nothing written), then --save
groundrag calibrate --collection handbook --gold data/eval/gold_handbook.jsonl
groundrag calibrate --collection handbook --gold data/eval/gold_handbook.jsonl --save
# 5. measure quality, inspect, clean up
groundrag eval --gold data/eval/gold_handbook.jsonl --collection handbook --no-ingest
groundrag stats # collections, rows, calibration state
groundrag drop --collection handbook # delete a collection (asks to confirm)
More CLI examples:
# these queries use the LLM: run the export from step 3 first
groundrag query "vacation days" --filter format=docx --filter date_from=2026-01-01
groundrag query "salary band?" --groups hr-admins
groundrag query "and the RX-400?" --history chat.json --json
groundrag --set rerank.top_k=8 query "..." # override any config key
groundrag ingest docs/ --collection handbook --prune # incremental; --rebuild to reset
Architecture
ingest ─► clean/dedup ─► translate to English (when active; keeps original) ─► chunk (recursive|semantic|parent-child, metadata) ─► embed (normalised)
─► LanceDB: vectors (flat→HNSW) + BM25 FTS (+ BM25 on original_text) + scalar/ACL columns + manifest
query ─► cleanup ─► translate to English (when active) ─► standalone rewrite ─► multi-query / HyDE ─► routing
─► recall: dense + BM25 per variant, pre-filtered ─► RRF (k=60) ─► top-50
─► rerank: cross-encoder ─► top-5 ─► score < min_score ? "not found"
─► context "[n] source — page N" (token budget) ─► generate (T=0.1, cited, 1 retry)
eval ─► gold.jsonl ─► retrieval + answer metrics ─► JSON report + RAGAS JSONL ─► gate
| Module | Path |
|---|---|
| Config / logging / models | src/groundrag/{config,logging_utils,models,tokens,text}.py |
| Ingest (pdf, docx, pptx, xlsx, csv, tsv, md, html, txt, rst, eml, epub, json, xml) | src/groundrag/ingest/ |
| Chunking | src/groundrag/chunking/ |
Embeddings (FastEmbed bge-small-en-v1.5, hashing fallback) |
src/groundrag/embeddings/ |
| Index (LanceDB, safe filters, ACL) | src/groundrag/index/ |
| Translation (language detection, NMT tiers, placeholders, cache) | src/groundrag/translation/ |
| Query transforms | src/groundrag/query/ |
| Hybrid retrieval + metrics | src/groundrag/retrieval/ |
| Rerankers | src/groundrag/rerank/ |
| LLM client, context, generator | src/groundrag/generation/ |
| Façade | src/groundrag/pipeline.py |
| Evaluation | src/groundrag/eval/, data/eval/gold.jsonl |
| CLI / API / MCP server | src/groundrag/cli.py, src/groundrag/api.py, src/groundrag/mcp_server.py |
Every log line is JSON with trace_id and per-stage timings_ms.
Agent skills
The repository ships Agent Skills in skills/
that teach a coding agent how to use groundrag: groundrag-cli, groundrag-python-integration,
groundrag-ingest-parsers, groundrag-tuning and groundrag-evaluation. Install them with:
npx skills add andrea9293/groundrag # pick the skills interactively
npx skills add andrea9293/groundrag --skill groundrag-cli # or a single one
Configuration
Full per-key reference (allowed values, defaults, effects): docs/CONFIG.md.
Most important keys (code defaults; config.yaml has the same values except where noted):
| Key | Default | Meaning |
|---|---|---|
ingest.parser |
hybrid |
hybrid | docling | markitdown | native (extras [parsers]) |
ingest.extensions |
[] in code; 16 formats in config.yaml |
files that are ingested |
chunking.strategy / chunk_size / chunk_overlap |
recursive / 400 / 60 (config.yaml: parent_child / 150 / 30) |
how text is split |
embeddings.backend / model |
auto / BAAI/bge-small-en-v1.5 |
FastEmbed, hashing fallback |
translation.enabled / tier / languages |
auto / fast / [] |
translate to English when the [translation] extra is installed; true | false to force |
retrieval.dense_top_k / sparse_top_k / candidates_top_k |
50 / 50 / 50 | recall stage (dense + BM25 + RRF) |
rerank.backend / model |
auto / Xenova/ms-marco-MiniLM-L-6-v2 |
cross-encoder, lexical fallback |
rerank.top_k |
5 | chunks kept after reranking |
rerank.min_score.cross_encoder / min_score_llm.cross_encoder |
0.5 / 0.05 | "not found" thresholds without / with an LLM |
llm.backend / model / base_url / api_key_env |
auto / gpt-4o-mini / null / OPENAI_API_KEY |
any OpenAI-compatible endpoint; the key is never read from the file |
query.multi_query.n / query.hyde.enabled |
3 / false |
LLM query expansion |
generation.temperature / max_tokens / max_context_tokens |
0.1 / 2000 / 3000 | answer building |
llm.max_tokens / llm.reasoning_effort |
6000 / null |
budget of the judge, gold-generate and query steps (reasoning models); optional reasoning effort |
eval.judge / eval.thresholds |
auto / {} in code; 8 gates in config.yaml |
eval judge and pass/fail gate |
security.enforce_acl |
true |
ACL filtering |
api.* / mcp.* |
127.0.0.1:8000, /ingest off / stdio, ingest off |
HTTP API and MCP server |
Justifications for the defaults: docs/DESIGN.md §2.
Limitations
- Offline fallbacks. Without access to HuggingFace / an LLM API the
autobackends use a hashing embedder, a lexical reranker and an extractive answerer (all logged as WARNING). They're deterministic and pass the eval gate on the sample set, but they don't understand synonyms (e.g. "EU" vs "European Union"). For production quality, use FastEmbed models and an LLM. - The token count is a regex estimate unless
tokens.counter: tiktoken. The contextual header (title > section) sits on top ofchunk_size. - Translation. Translation quality is that of the NMT model: tables, names and short queries can
come out wrong, which cost a few answers on same-language Italian questions in the evaluation
(one run, 13 questions). Only English is a target language. Routing keywords must be in English
while translation is active. The first ingest after translation becomes active re-chunks the
documents it sees; unseen ones need
--rebuildor--prune. - The BM25 index is rebuilt after each ingest batch, which is fine up to roughly 10⁵ chunks. For larger corpora, batch your ingests.
- The HTTP API has no authentication. Run it behind a gateway. The MCP server over HTTP requires OAuth (or an explicit opt-out for use behind a gateway).
- The MCP Tasks extension is not yet supported by the Python SDK, so MCP
ingest_documentsis synchronous (and disabled by default). - The gold set is small (23 questions: 20 answerable and 3 unanswerable traps) and synthetic. Extend it with real questions before relying on it for tuning.
- "Not found" thresholds. Without an LLM no threshold can reject questions that are on topic
but have no answer in the documents (the cross-encoder scores them ~0.98); only an LLM does
that.
groundrag calibratereports how many it would reject (trap_rejection) instead of hiding it. With few gold questions (< 30 answerable) the threshold is flaggedfew_data. - Generated gold sets (
groundrag gold-generate) are easier than real questions and are markedneeds_review. Use them as a starting point and recalibrate with real questions.
Metadata
Release files for groundrag 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| groundrag-0.3.0.tar.gz | 155.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| groundrag-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 275.2 kB
Release files / groundrag-0.3.0.tar.gz
| Download URL | groundrag-0.3.0.tar.gz |
|---|---|
| Size | 155.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d0dbb49de15fc89a10521470a997b09bee0909ad038671075335a015d6704c2
|
|
BLAKE2b-256 checksum How to use checksums |
af8ad8fba42859c29dba59fb626b633c1b533bcd2413338d2887765544856616
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / groundrag-0.3.0-py3-none-any.whl
| Download URL | groundrag-0.3.0-py3-none-any.whl |
|---|---|
| Size | 119.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
001ee8338478cfb1cfcfa5f8a0399edec5b595427f30fa33c3b941437656d909
|
|
BLAKE2b-256 checksum How to use checksums |
5ce2957a603efbc2bc4bee815bf4cd2a70a6b6637b3df76e5971eb308c54b5a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for groundrag 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for groundrag-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.