ragaudit-cli 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ragaudit_cli-0.3.0/.gitignore +22 -0
- ragaudit_cli-0.3.0/CHANGELOG.md +99 -0
- ragaudit_cli-0.3.0/LICENSE +21 -0
- ragaudit_cli-0.3.0/PKG-INFO +429 -0
- ragaudit_cli-0.3.0/README.md +385 -0
- ragaudit_cli-0.3.0/pyproject.toml +79 -0
- ragaudit_cli-0.3.0/src/ragaudit/__init__.py +1 -0
- ragaudit_cli-0.3.0/src/ragaudit/adapters/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/adapters/base.py +109 -0
- ragaudit_cli-0.3.0/src/ragaudit/adapters/callable_adapter.py +15 -0
- ragaudit_cli-0.3.0/src/ragaudit/cli.py +540 -0
- ragaudit_cli-0.3.0/src/ragaudit/cli_easy.py +458 -0
- ragaudit_cli-0.3.0/src/ragaudit/cli_v2.py +507 -0
- ragaudit_cli-0.3.0/src/ragaudit/env.py +46 -0
- ragaudit_cli-0.3.0/src/ragaudit/generate/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/generate/pipeline.py +486 -0
- ragaudit_cli-0.3.0/src/ragaudit/generate/prompts.py +116 -0
- ragaudit_cli-0.3.0/src/ragaudit/generate/testset_generator.py +146 -0
- ragaudit_cli-0.3.0/src/ragaudit/generate/text.py +75 -0
- ragaudit_cli-0.3.0/src/ragaudit/ingest/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/ingest/chunker.py +53 -0
- ragaudit_cli-0.3.0/src/ragaudit/ingest/corpus.py +120 -0
- ragaudit_cli-0.3.0/src/ragaudit/ingest/loader.py +49 -0
- ragaudit_cli-0.3.0/src/ragaudit/ingest/versioning.py +92 -0
- ragaudit_cli-0.3.0/src/ragaudit/llm/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/llm/client.py +86 -0
- ragaudit_cli-0.3.0/src/ragaudit/llm/factory.py +70 -0
- ragaudit_cli-0.3.0/src/ragaudit/llm/json_output.py +72 -0
- ragaudit_cli-0.3.0/src/ragaudit/llm/mock.py +552 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/adapter.py +39 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/document.py +28 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/evaluation.py +70 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/question.py +152 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/run.py +44 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/scoring.py +82 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/testset.py +30 -0
- ragaudit_cli-0.3.0/src/ragaudit/models/tune.py +56 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/generator.py +155 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/plain_summary.py +122 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/_v2_tooltip.jinja +43 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/generate_report.html.jinja +175 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/report.html.jinja +252 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/style.css +1095 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/tune_report.html.jinja +158 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/templates/v2.css +286 -0
- ragaudit_cli-0.3.0/src/ragaudit/report/v2.py +380 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/ablation.py +46 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/evaluate.py +325 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/executor.py +98 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/locked.py +37 -0
- ragaudit_cli-0.3.0/src/ragaudit/runner/summary.py +45 -0
- ragaudit_cli-0.3.0/src/ragaudit/scaffold.py +252 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/correctness.py +94 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/groundedness.py +86 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/retrieval.py +42 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/text_match.py +51 -0
- ragaudit_cli-0.3.0/src/ragaudit/scoring/verdict.py +14 -0
- ragaudit_cli-0.3.0/src/ragaudit/storage/__init__.py +0 -0
- ragaudit_cli-0.3.0/src/ragaudit/storage/db.py +81 -0
- ragaudit_cli-0.3.0/src/ragaudit/storage/lock.py +36 -0
- ragaudit_cli-0.3.0/src/ragaudit/storage/repository.py +216 -0
- ragaudit_cli-0.3.0/src/ragaudit/storage/store.py +536 -0
- ragaudit_cli-0.3.0/src/ragaudit/tune/__init__.py +7 -0
- ragaudit_cli-0.3.0/src/ragaudit/tune/devset.py +16 -0
- ragaudit_cli-0.3.0/src/ragaudit/tune/loop.py +208 -0
- ragaudit_cli-0.3.0/src/ragaudit/tune/search_space.py +140 -0
- ragaudit_cli-0.3.0/src/ragaudit/tune/strategies.py +197 -0
- ragaudit_cli-0.3.0/src/ragaudit/yaml_io.py +199 -0
- ragaudit_cli-0.3.0/tests/conftest.py +7 -0
- ragaudit_cli-0.3.0/tests/fixtures/adapter_raises_on_import.py +1 -0
- ragaudit_cli-0.3.0/tests/fixtures/adapter_with_neither.py +2 -0
- ragaudit_cli-0.3.0/tests/fixtures/adapter_with_object.py +9 -0
- ragaudit_cli-0.3.0/tests/fixtures/adapter_with_query.py +5 -0
- ragaudit_cli-0.3.0/tests/test_ablation.py +97 -0
- ragaudit_cli-0.3.0/tests/test_adapter_loading.py +37 -0
- ragaudit_cli-0.3.0/tests/test_check.py +269 -0
- ragaudit_cli-0.3.0/tests/test_chunker.py +44 -0
- ragaudit_cli-0.3.0/tests/test_cli.py +168 -0
- ragaudit_cli-0.3.0/tests/test_correctness.py +54 -0
- ragaudit_cli-0.3.0/tests/test_demo_adapter.py +127 -0
- ragaudit_cli-0.3.0/tests/test_doctor.py +196 -0
- ragaudit_cli-0.3.0/tests/test_executor.py +118 -0
- ragaudit_cli-0.3.0/tests/test_groundedness.py +40 -0
- ragaudit_cli-0.3.0/tests/test_init.py +159 -0
- ragaudit_cli-0.3.0/tests/test_llm_client.py +81 -0
- ragaudit_cli-0.3.0/tests/test_match_by_text.py +226 -0
- ragaudit_cli-0.3.0/tests/test_report_generator.py +361 -0
- ragaudit_cli-0.3.0/tests/test_repository.py +163 -0
- ragaudit_cli-0.3.0/tests/test_retrieval.py +37 -0
- ragaudit_cli-0.3.0/tests/test_summary.py +143 -0
- ragaudit_cli-0.3.0/tests/test_testset_generator.py +152 -0
- ragaudit_cli-0.3.0/tests/test_v2_evaluate.py +302 -0
- ragaudit_cli-0.3.0/tests/test_v2_generate.py +386 -0
- ragaudit_cli-0.3.0/tests/test_v2_llm_output.py +176 -0
- ragaudit_cli-0.3.0/tests/test_v2_locked_test_set.py +81 -0
- ragaudit_cli-0.3.0/tests/test_v2_reports.py +246 -0
- ragaudit_cli-0.3.0/tests/test_v2_split.py +256 -0
- ragaudit_cli-0.3.0/tests/test_v2_store.py +172 -0
- ragaudit_cli-0.3.0/tests/test_v2_tune.py +346 -0
- ragaudit_cli-0.3.0/tests/test_v2_versioning.py +118 -0
- ragaudit_cli-0.3.0/tests/test_verdict.py +18 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
.env
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
*.db
|
|
14
|
+
*.sqlite3
|
|
15
|
+
.DS_Store
|
|
16
|
+
.idea/
|
|
17
|
+
.vscode/
|
|
18
|
+
|
|
19
|
+
demo/output/*
|
|
20
|
+
!demo/output/.gitkeep
|
|
21
|
+
.ragaudit/
|
|
22
|
+
demo/v2/output/
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
Easy mode: a first-time user can test their own pipeline in a few
|
|
6
|
+
minutes, without re-indexing it. Existing commands, flags and defaults
|
|
7
|
+
are unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `--match-by id|text` (and `--match-threshold`, default 0.5) on `run`
|
|
12
|
+
and `evaluate`. Text mode counts a retrieval hit when one of the
|
|
13
|
+
contexts the adapter returns overlaps the gold chunk's text (overlap of
|
|
14
|
+
word pairs, measured against the shorter side), so pipelines that chunk
|
|
15
|
+
the corpus themselves work as they are. `id` stays the default. In
|
|
16
|
+
`evaluate`, text mode needs `--corpus` and is cached as its own config;
|
|
17
|
+
id-mode cache keys are unchanged.
|
|
18
|
+
- `ragaudit init --framework plain|langchain|llamaindex`: a commented
|
|
19
|
+
adapter with one TODO where the pipeline plugs in, plus a `.env.example`
|
|
20
|
+
for OpenAI-compatible APIs and local Ollama.
|
|
21
|
+
- `ragaudit init --framework demo`: a ready-to-run toy pipeline (keyword
|
|
22
|
+
search over the docs folder, top 3 paragraphs, answered by the configured
|
|
23
|
+
`RAGAUDIT_*` LLM) for seeing a full report before wiring up your own.
|
|
24
|
+
`check` points it at its `--docs` folder and counts its answer calls in
|
|
25
|
+
the estimate. `--llm` is now passed on to adapters that use ragaudit's
|
|
26
|
+
LLM, in `check` and `doctor`.
|
|
27
|
+
- `ragaudit doctor`: PASS/FAIL with an exact fix for the Python version,
|
|
28
|
+
the `RAGAUDIT_*` settings (whether set, never their values), one test
|
|
29
|
+
call to the API, the adapter import, and a valid `RAGResponse` on a
|
|
30
|
+
sample question.
|
|
31
|
+
- `ragaudit check --docs PATH --adapter FILE`: ingest, generate (20
|
|
32
|
+
questions by default, `--questions`), run with ablation and report in
|
|
33
|
+
one command, matching by text unless `--match-by id`. It tries the
|
|
34
|
+
adapter once first, prints the estimated LLM calls and asks before
|
|
35
|
+
spending (`--yes` to skip), stops at a budget cap (`--max-llm-calls`,
|
|
36
|
+
default the estimate's upper bound), opens the report (`--no-open` to
|
|
37
|
+
skip), and prints a summary of at most six lines.
|
|
38
|
+
- The same plain-English summary (verdict, retrieval hit rate, answer
|
|
39
|
+
correctness, lucky passes, most useful fix) at the top of the HTML run
|
|
40
|
+
report. The docs sample reports are re-rendered with it.
|
|
41
|
+
- All commands read `RAGAUDIT_*` settings from `./.env`; exported
|
|
42
|
+
variables take priority.
|
|
43
|
+
- Published on PyPI as `ragaudit-cli` (`pip install ragaudit-cli`;
|
|
44
|
+
the YAML extra is `pip install "ragaudit-cli[yaml]"`). The command and
|
|
45
|
+
the import package are still `ragaudit`.
|
|
46
|
+
- PyPI packaging metadata and a release workflow
|
|
47
|
+
(`.github/workflows/publish.yml`) that publishes on a `vX.Y.Z` tag via
|
|
48
|
+
trusted publishing.
|
|
49
|
+
- "Test your RAG in 5 minutes" at the top of the README and the landing
|
|
50
|
+
page.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- The source distribution contained only the report templates, with no
|
|
55
|
+
Python code.
|
|
56
|
+
- Two report tests depended on gitignored demo databases, so CI failed.
|
|
57
|
+
- Long ids in the run report header, and the landing page's install
|
|
58
|
+
section, no longer widen the page on phones.
|
|
59
|
+
|
|
60
|
+
## 0.2.0
|
|
61
|
+
|
|
62
|
+
v2: generate, evaluate and tune with honest scores. Everything in 0.1.0
|
|
63
|
+
works as before; v1 commands, flags, outputs and reports are unchanged.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- `ragaudit generate --corpus PATH`: fact extraction per chunk, de-worded
|
|
68
|
+
questions with a lexical-overlap check, a relative-time check, and
|
|
69
|
+
verification by a second model. Rejected questions keep a reason.
|
|
70
|
+
Optional multi-hop questions (`--multi-hop`). Budgets: `--max-chunks`,
|
|
71
|
+
`--max-llm-calls`. Re-runs skip chunks that already have questions.
|
|
72
|
+
- A v2 store (`.ragaudit/store.db`) with a per-chunk dev (70% of chunks) /
|
|
73
|
+
test (30%) split: each chunk's split comes from a hash of its id, and
|
|
74
|
+
all its questions inherit it, so no chunk feeds both splits. Multi-hop
|
|
75
|
+
questions spanning both splits are rejected as `cross_split`. The test
|
|
76
|
+
split is locked: reading it raises unless the single final-evaluation
|
|
77
|
+
path unlocks it.
|
|
78
|
+
- Corpus versioning: each corpus is fingerprinted, and edited or deleted
|
|
79
|
+
chunks mark their questions stale (reverting an edit revives them).
|
|
80
|
+
- `ragaudit evaluate`: scores a pipeline on the dev split (or, explicitly,
|
|
81
|
+
the test split) with results cached per question, config and corpus
|
|
82
|
+
version; regression mode re-runs new questions plus a seeded sample of
|
|
83
|
+
old ones when only the corpus changed.
|
|
84
|
+
- `ragaudit tune`: grid, seeded random or LLM-proposed configs over a YAML
|
|
85
|
+
search space, evaluated on dev; baseline and best are scored once on the
|
|
86
|
+
locked test split, with an overfitting warning. Writes the best config
|
|
87
|
+
as YAML.
|
|
88
|
+
- `ragaudit report --kind generate|tune`: HTML reports for generate and
|
|
89
|
+
tune runs.
|
|
90
|
+
- `--llm mock`: an offline, deterministic mock LLM for demos and tests.
|
|
91
|
+
- `demo/v2/`: a 48-chunk invented corpus, a corpus update, a configurable
|
|
92
|
+
toy pipeline, a search space and `run_demo.sh`.
|
|
93
|
+
- Optional extra `ragaudit[yaml]` (PyYAML); a built-in parser handles
|
|
94
|
+
simple YAML without it.
|
|
95
|
+
|
|
96
|
+
## 0.1.0
|
|
97
|
+
|
|
98
|
+
- Initial release: ingest, generate, run (with ablation), report, and the
|
|
99
|
+
LUCKY_PASS verdict.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Ekramy Ismail
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ragaudit-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Test a RAG pipeline in minutes: catch correct answers that retrieval didn't earn (lucky passes), without re-indexing.
|
|
5
|
+
Project-URL: Homepage, https://aliismail293.github.io/rag-tester/
|
|
6
|
+
Project-URL: Documentation, https://github.com/Aliismail293/rag-tester#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Aliismail293/rag-tester
|
|
8
|
+
Project-URL: Issues, https://github.com/Aliismail293/rag-tester/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Aliismail293/rag-tester/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Ali Ekramy Ismail <alii.ekramy.ismail@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: evaluation,langchain,llamaindex,llm,ollama,rag,retrieval-augmented-generation,testing
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
26
|
+
Classifier: Topic :: Software Development :: Testing
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: httpx>=0.27
|
|
29
|
+
Requires-Dist: jinja2>=3.1
|
|
30
|
+
Requires-Dist: pydantic>=2.0
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
37
|
+
Provides-Extra: langchain
|
|
38
|
+
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
39
|
+
Provides-Extra: llamaindex
|
|
40
|
+
Requires-Dist: llama-index-core>=0.10; extra == 'llamaindex'
|
|
41
|
+
Provides-Extra: yaml
|
|
42
|
+
Requires-Dist: pyyaml>=6; extra == 'yaml'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# ragaudit
|
|
46
|
+
|
|
47
|
+
## Test your RAG in 5 minutes
|
|
48
|
+
|
|
49
|
+
You need Python 3.11+, a folder of your documents (`.txt` or `.md`), and an
|
|
50
|
+
LLM for ragaudit to write questions and judge answers: any OpenAI-compatible
|
|
51
|
+
API, or a local [Ollama](https://ollama.com). Your pipeline is not re-indexed.
|
|
52
|
+
|
|
53
|
+
**0. No pipeline yet? Use `--framework demo` to see a full report in 2 minutes.**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install ragaudit-cli
|
|
57
|
+
mkdir docs && cp ~/notes/*.md docs/ # any few .txt or .md files
|
|
58
|
+
ragaudit init --framework demo
|
|
59
|
+
ragaudit check --docs ./docs --adapter ragaudit_adapter.py --questions 5
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The demo adapter is a small, complete pipeline (paragraphs from `./docs`, top 3
|
|
63
|
+
by word overlap, answered by the same LLM ragaudit uses), so there is nothing to
|
|
64
|
+
edit. Set the three `RAGAUDIT_*` settings from step 2 first, or add `--llm mock`
|
|
65
|
+
to try it offline with a fake LLM. Then follow steps 1 to 5 for your own pipeline.
|
|
66
|
+
|
|
67
|
+
**1. Install**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install ragaudit-cli
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The package is `ragaudit-cli` on PyPI; the command and the Python import are
|
|
74
|
+
`ragaudit`. (Until the first PyPI release: `pip install git+https://github.com/Aliismail293/rag-tester`.)
|
|
75
|
+
|
|
76
|
+
**2. Check your setup**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export RAGAUDIT_API_BASE=https://api.openai.com/v1 # Ollama: http://localhost:11434/v1
|
|
80
|
+
export RAGAUDIT_API_KEY=sk-... # Ollama: ollama
|
|
81
|
+
export RAGAUDIT_MODEL=gpt-4o-mini # Ollama: llama3.1:8b
|
|
82
|
+
ragaudit doctor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each check prints PASS or FAIL with the exact fix. The adapter check fails
|
|
86
|
+
until step 4. Settings can also go in a `.env` file (step 3 writes an example).
|
|
87
|
+
|
|
88
|
+
**3. Create an adapter**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ragaudit init --framework plain # or langchain, llamaindex
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This writes `ragaudit_adapter.py` and `.env.example`.
|
|
95
|
+
|
|
96
|
+
**4. Fill in the adapter**
|
|
97
|
+
|
|
98
|
+
Open `ragaudit_adapter.py` and connect your pipeline at the one `TODO`: return
|
|
99
|
+
its answer and the text of the passages it retrieved. Run `ragaudit doctor`
|
|
100
|
+
again until every line says PASS.
|
|
101
|
+
|
|
102
|
+
**5. Run the check**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
ragaudit check --docs ./docs --adapter ragaudit_adapter.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
It shows how many LLM calls it will make (about 3 per question, 20 questions
|
|
109
|
+
by default) and asks before starting. Then it writes questions from your docs,
|
|
110
|
+
runs them through your pipeline, opens an HTML report, and prints a summary
|
|
111
|
+
like this:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
ragaudit check: summary
|
|
115
|
+
Verdict: NEEDS WORK. Only 4 of 13 answers were correct and from the right passage; main problem: ...
|
|
116
|
+
Retrieval hit rate: 31% (4 of 13 found the passage the question came from, matched by text)
|
|
117
|
+
Answer correctness: 100% (13 of 13 judged correct)
|
|
118
|
+
Lucky passes: 9 (correct answer, but the right passage was not retrieved); ...
|
|
119
|
+
Most useful fix: don't trust the correct answers yet. ...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Reading the four verdicts.** Every question gets one:
|
|
123
|
+
|
|
124
|
+
| Verdict | Answer | Right passage retrieved? | What it means |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `TRUE_PASS` | correct | yes | The pipeline worked. This is the number to protect. |
|
|
127
|
+
| `LUCKY_PASS` | correct | no | The model answered from memory; retrieval did nothing. Other evals count this as a pass. |
|
|
128
|
+
| `GENERATION_FAILURE` | wrong | yes | Retrieval worked; the prompt or model didn't use it. |
|
|
129
|
+
| `RETRIEVAL_FAILURE` | wrong | no | Retrieval missed. Raise top-k, add keyword/hybrid search, or change chunking. |
|
|
130
|
+
|
|
131
|
+
## Try it in 60 seconds
|
|
132
|
+
|
|
133
|
+
Requires Docker and an OpenAI-compatible API key.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export RAGAUDIT_API_KEY=sk-...
|
|
137
|
+
export RAGAUDIT_API_BASE=https://api.openai.com/v1 # any OpenAI-compatible endpoint
|
|
138
|
+
export RAGAUDIT_MODEL=gpt-4o-mini
|
|
139
|
+
|
|
140
|
+
docker compose up --build
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This runs three scenarios and writes three reports to `demo/output/`:
|
|
144
|
+
|
|
145
|
+
| Report | Corpus | Adapter | What it shows |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| `report-healthy.html` | `demo/sample_corpus/` (invented facts) | `example_adapter.py` | Retrieval working, answers grounded. |
|
|
148
|
+
| `report-degraded.html` | `demo/sample_corpus/` (invented facts) | `weak_adapter.py` | Retrieval quietly broken (30% random chunks), failures visible. |
|
|
149
|
+
| `report-lucky.html` | `demo/lucky_corpus/` (well-known facts) | `ungrounded_adapter.py` | Answers correct anyway, retrieval contributing nothing. |
|
|
150
|
+
|
|
151
|
+
`demo/sample_corpus/` (18 chunks, deliberately overlapping topics —
|
|
152
|
+
multiple chunks touch the same product or the same SLA terms) describes
|
|
153
|
+
an invented company, product, and internal processes — facts that don't
|
|
154
|
+
exist anywhere for an LLM to have learned during pretraining. That's
|
|
155
|
+
deliberate: any answer an adapter gets right in the first two reports
|
|
156
|
+
had to actually come from the retrieved context, so a `LUCKY_PASS` there
|
|
157
|
+
would be a real, meaningful finding, not an artifact of the model
|
|
158
|
+
already knowing the answer. Comparing `report-healthy.html` against
|
|
159
|
+
`report-degraded.html` — same test set, same generation model, only
|
|
160
|
+
retrieval quality changed — shows how much retrieval quality alone moves
|
|
161
|
+
the verdict breakdown.
|
|
162
|
+
|
|
163
|
+
`demo/lucky_corpus/` (13 chunks: world capitals, the periodic table and
|
|
164
|
+
other basic chemistry, famous historical dates, basic physics constants)
|
|
165
|
+
flips the premise: these are facts any LLM already knows cold. It's
|
|
166
|
+
ingested into its own database (`ragaudit-lucky.db`) rather than sharing
|
|
167
|
+
the sample corpus's, so ingesting it can't overwrite chunks the other two
|
|
168
|
+
reports depend on. Running `ungrounded_adapter.py` against it, with
|
|
169
|
+
ablation enabled, means retrieval frequently returns the wrong chunk, or
|
|
170
|
+
a random one — yet the model answers correctly anyway, from pretraining,
|
|
171
|
+
and ablation (re-asking with no context at all) confirms it stayed
|
|
172
|
+
correct. `report-lucky.html` is what a corpus full of `LUCKY_PASS`
|
|
173
|
+
actually looks like: the case ragaudit exists to catch, that a
|
|
174
|
+
correctness-only eval would score as a clean pass.
|
|
175
|
+
|
|
176
|
+
- `demo/example_adapter.py` — keyword/BM25 retrieval, no embeddings, no
|
|
177
|
+
vector DB, top-1 only — "healthy but mediocre."
|
|
178
|
+
- `demo/weak_adapter.py` — identical retrieval, except a seeded 30% of
|
|
179
|
+
queries get a random chunk instead of the true top match, simulating a
|
|
180
|
+
retrieval index that has quietly gone stale — "degraded." Its
|
|
181
|
+
generation prompt strictly says "only use the provided context," so a
|
|
182
|
+
bad retrieval mostly shows up as a visible failure, not a lucky guess.
|
|
183
|
+
- `demo/ungrounded_adapter.py` — same retrieval logic, degraded further
|
|
184
|
+
(70% random), but with a permissive generation prompt: the context is
|
|
185
|
+
offered as background, and the model is told to answer from its own
|
|
186
|
+
knowledge if the context doesn't help. That prompt difference is the
|
|
187
|
+
whole point — it's what turns bad retrieval into a *hidden* failure
|
|
188
|
+
instead of a visible one, which is exactly what `report-lucky.html`
|
|
189
|
+
is built to catch.
|
|
190
|
+
|
|
191
|
+
Audits RAG pipelines by checking whether retrieval actually did the work —
|
|
192
|
+
not just whether the final answer was correct.
|
|
193
|
+
|
|
194
|
+
A RAG system can return a correct answer while retrieving the wrong
|
|
195
|
+
documents, because the model answered from pretraining rather than from
|
|
196
|
+
context. Standard evals score that as a pass. ragaudit catches it and
|
|
197
|
+
labels it `LUCKY_PASS`.
|
|
198
|
+
|
|
199
|
+
## Status
|
|
200
|
+
|
|
201
|
+
Functional end to end: ingestion, test-set generation, running, scoring
|
|
202
|
+
(including ablation), storage, reporting, and the CLI are all implemented.
|
|
203
|
+
See `demo/` for a working example.
|
|
204
|
+
|
|
205
|
+
v2 (0.2.0) adds question generation with verification, cached and
|
|
206
|
+
regression evaluation, and config tuning with a locked test split; see
|
|
207
|
+
[v2: generate, evaluate, tune](#v2-generate-evaluate-tune).
|
|
208
|
+
|
|
209
|
+
## How it works
|
|
210
|
+
|
|
211
|
+
1. Ingest a corpus of documents; ragaudit assigns stable chunk IDs.
|
|
212
|
+
2. Generate a test set: an LLM writes questions whose answers live in each
|
|
213
|
+
chunk, storing `(question, expected_answer, source_chunk_id)`.
|
|
214
|
+
3. Run your RAG pipeline through an adapter.
|
|
215
|
+
4. Score answer correctness, retrieval hit rate, and groundedness.
|
|
216
|
+
5. Cross-reference correctness against retrieval:
|
|
217
|
+
- correct + retrieved → `TRUE_PASS`
|
|
218
|
+
- correct + NOT retrieved → `LUCKY_PASS` (the headline output)
|
|
219
|
+
- wrong + retrieved → `GENERATION_FAILURE`
|
|
220
|
+
- wrong + NOT retrieved → `RETRIEVAL_FAILURE`
|
|
221
|
+
6. For candidate `LUCKY_PASS` cases, re-run the question with retrieval
|
|
222
|
+
disabled. If the answer is still correct, retrieval contributed
|
|
223
|
+
nothing — this empirically confirms the verdict.
|
|
224
|
+
7. Emit a static HTML report.
|
|
225
|
+
|
|
226
|
+
## Matching retrieval: by id or by text
|
|
227
|
+
|
|
228
|
+
ragaudit needs to know whether your pipeline retrieved the passage a
|
|
229
|
+
question was written from. `run` and `evaluate` can decide that two ways,
|
|
230
|
+
chosen with `--match-by`:
|
|
231
|
+
|
|
232
|
+
**`--match-by id` (the default).** A hit means the `source_chunk_id`
|
|
233
|
+
ragaudit assigned during ingestion appears in the `retrieved_chunk_ids`
|
|
234
|
+
your adapter returns. This only works if your pipeline is indexed against
|
|
235
|
+
the chunks ragaudit produced:
|
|
236
|
+
|
|
237
|
+
1. Run `ragaudit ingest ./docs` to chunk your corpus and assign IDs.
|
|
238
|
+
2. Index *those chunks* into your own pipeline, so retrieved documents
|
|
239
|
+
carry ragaudit's chunk IDs as metadata.
|
|
240
|
+
3. Your adapter's `query()` returns those same IDs in `retrieved_chunk_ids`.
|
|
241
|
+
|
|
242
|
+
If your pipeline chunks the corpus itself, id matching reads retrieval as
|
|
243
|
+
0% and misreports every correct answer as `LUCKY_PASS`, because the tool
|
|
244
|
+
can't recognize IDs it didn't assign.
|
|
245
|
+
|
|
246
|
+
**`--match-by text`.** A hit means one of the `contexts` your adapter
|
|
247
|
+
already returns overlaps the gold chunk's text. Your pipeline can keep its
|
|
248
|
+
own chunking and IDs; nothing is re-indexed. The score is the share of the
|
|
249
|
+
gold chunk's word pairs found in the context, measured against whichever
|
|
250
|
+
side is shorter, so a big chunk that contains the gold passage matches and
|
|
251
|
+
so does a smaller chunk cut from inside it. Case, punctuation and
|
|
252
|
+
whitespace are ignored. The default threshold is 0.5; change it with
|
|
253
|
+
`--match-threshold`. `ragaudit check` uses text matching by default.
|
|
254
|
+
|
|
255
|
+
| | `--match-by id` | `--match-by text` |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| Setup | index ragaudit's chunks, return their ids | return the retrieved texts in `contexts` |
|
|
258
|
+
| Precision | exact: same chunk or not | approximate: a threshold on word overlap |
|
|
259
|
+
| False hits | none | a near-duplicate passage elsewhere in the corpus can count |
|
|
260
|
+
| False misses | none, if ids are returned | heavy rewriting of chunks (summaries, translations) can miss |
|
|
261
|
+
| `evaluate` needs | nothing extra | `--corpus`, for the gold chunk texts |
|
|
262
|
+
|
|
263
|
+
Use id matching when you can index ragaudit's chunks; use text matching to
|
|
264
|
+
test the pipeline you already have. The two modes are cached as separate
|
|
265
|
+
configs in `evaluate`, so switching never reuses the other mode's results.
|
|
266
|
+
|
|
267
|
+
## Adapter interface
|
|
268
|
+
|
|
269
|
+
Implement one function:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
def query(question: str) -> RAGResponse:
|
|
273
|
+
...
|
|
274
|
+
|
|
275
|
+
# RAGResponse = { answer: str, retrieved_chunk_ids: list[str], contexts: list[str] }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Adapters ship for: a raw callable, LangChain, LlamaIndex.
|
|
279
|
+
|
|
280
|
+
## CLI
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
ragaudit init --framework plain # starter adapter (plain|langchain|llamaindex|demo) + .env.example
|
|
284
|
+
ragaudit doctor # check Python, RAGAUDIT_* settings, one LLM call, and the adapter
|
|
285
|
+
ragaudit check --docs ./docs --adapter ragaudit_adapter.py # all of the below in one go, then open the report
|
|
286
|
+
ragaudit ingest ./docs # load, chunk, and persist a corpus
|
|
287
|
+
ragaudit generate --n 100 # generate a test set (n questions/chunk)
|
|
288
|
+
ragaudit run --adapter my_adapter.py # run + score + ablate against the latest test set
|
|
289
|
+
ragaudit run --adapter my_adapter.py --match-by text # same, matching retrieval by context text
|
|
290
|
+
ragaudit report --out report.html # render the latest run as a static HTML report
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Every command takes `--db` (default `./ragaudit.db`) to point at a
|
|
294
|
+
specific database file. All commands read `RAGAUDIT_*` settings from the
|
|
295
|
+
environment or from a `.env` file in the current folder (exported
|
|
296
|
+
variables win); `ragaudit init` writes a `.env.example` to copy.
|
|
297
|
+
|
|
298
|
+
## v2: generate, evaluate, tune
|
|
299
|
+
|
|
300
|
+
v1 audits one run. v2 adds a loop for improving a pipeline without fooling
|
|
301
|
+
yourself: build a question set that can't be gamed, evaluate cheaply as the
|
|
302
|
+
corpus changes, and let ragaudit search pipeline configs while keeping a
|
|
303
|
+
test split it never tunes on.
|
|
304
|
+
|
|
305
|
+
### What it does
|
|
306
|
+
|
|
307
|
+
- **`generate --corpus`** extracts facts from each chunk, writes a question
|
|
308
|
+
per fact, rewrites it away from the passage's wording, and has a second
|
|
309
|
+
model verify it is answerable, unambiguous and supported. Questions that
|
|
310
|
+
still copy the passage, use relative time ("recently", "last quarter"),
|
|
311
|
+
or fail verification are rejected with a reason. `--multi-hop` adds
|
|
312
|
+
questions that need two chunks.
|
|
313
|
+
- **`evaluate`** runs your pipeline on the dev split. Results are cached per
|
|
314
|
+
(question, config, corpus version), so an unchanged config re-scores
|
|
315
|
+
instantly. When the corpus changes, questions on edited or deleted chunks
|
|
316
|
+
go stale, and only new questions plus a seeded sample of old ones are
|
|
317
|
+
re-run.
|
|
318
|
+
- **`tune`** searches a declared space of pipeline knobs (grid, seeded
|
|
319
|
+
random, or an LLM that reads dev failures and proposes the next config),
|
|
320
|
+
then scores the baseline and the best config once each on the locked test
|
|
321
|
+
split.
|
|
322
|
+
- **`report --kind generate|tune`** renders both as self-contained HTML.
|
|
323
|
+
|
|
324
|
+
### Why the scores are honest
|
|
325
|
+
|
|
326
|
+
- **Questions don't share wording with their passage.** If over half of a
|
|
327
|
+
question's content words appear in its source chunk (configurable with
|
|
328
|
+
`--lexical-threshold`), it gets one more rewrite and is then rejected.
|
|
329
|
+
Keyword search alone can't ace the set.
|
|
330
|
+
- **A second model verifies every question.** The verifier must be a
|
|
331
|
+
different model from the generator.
|
|
332
|
+
- **The split is per chunk, fixed by hash.** Each chunk goes to dev (70% of
|
|
333
|
+
chunks) or test (30%) from a salted hash of its chunk id, and every
|
|
334
|
+
question from that chunk inherits its split, so no chunk feeds both
|
|
335
|
+
sides. Tuning on dev can't teach the pipeline a test chunk. Hashing the
|
|
336
|
+
id, not the content, means editing a chunk never moves it. A multi-hop
|
|
337
|
+
question is only kept when all its chunks share a split; otherwise it's
|
|
338
|
+
rejected as `cross_split` (and `generate` only pairs same-split chunks).
|
|
339
|
+
- **Tuning can't see the test split.** Reading test questions raises an
|
|
340
|
+
error unless one function, used only for the final score, unlocks it. A
|
|
341
|
+
test also parses the tuning code and fails if it imports the lock or the
|
|
342
|
+
database directly. The LLM proposer only ever sees dev data.
|
|
343
|
+
- **A pass needs retrieval and a correct answer.** The score is the share of
|
|
344
|
+
questions where the gold chunk was in the top k *and* the answer was
|
|
345
|
+
judged correct. A correct answer without the gold chunk counts as a
|
|
346
|
+
retrieval miss, so tuning can't reward a model answering from memory.
|
|
347
|
+
- **Overfitting is flagged.** If the best config's dev score beats its test
|
|
348
|
+
score by more than `--overfit-threshold` points (default 10), the run says
|
|
349
|
+
so.
|
|
350
|
+
|
|
351
|
+
### Quickstart
|
|
352
|
+
|
|
353
|
+
Offline, with the bundled demo and the deterministic mock LLM:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
pip install -e .
|
|
357
|
+
sh demo/v2/run_demo.sh # reports in demo/v2/output/
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Against your own corpus and pipeline, with a real model:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
export RAGAUDIT_API_KEY=sk-... RAGAUDIT_MODEL=gpt-4o-mini
|
|
364
|
+
ragaudit generate --corpus corpus.jsonl --verifier-model gpt-4o
|
|
365
|
+
ragaudit evaluate --adapter my_pipeline.py --corpus corpus.jsonl
|
|
366
|
+
ragaudit tune --adapter my_pipeline.py --search-space space.yaml --strategy llm
|
|
367
|
+
ragaudit report --kind tune --out tune-report.html
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
A corpus is a `.jsonl` file, one chunk per line:
|
|
371
|
+
`{"chunk_id": "...", "text": "...", "timestamp": "2026-01-12", "doc_id": "...", "position": 0}`
|
|
372
|
+
(only `chunk_id` and `text` are required), or a directory of `.txt`/`.md`
|
|
373
|
+
files, which get ids like `guide.md#3`. Chunk ids must stay stable across
|
|
374
|
+
edits so ragaudit can tell an edited chunk from a new one.
|
|
375
|
+
|
|
376
|
+
For `tune`, the adapter exposes `make_adapter(config) -> adapter` (see
|
|
377
|
+
`demo/v2/toy_adapter.py`); plain v1 adapters still work with `evaluate`.
|
|
378
|
+
The search space is YAML:
|
|
379
|
+
|
|
380
|
+
```yaml
|
|
381
|
+
knobs:
|
|
382
|
+
chunk_size: [200, 400, 800]
|
|
383
|
+
top_k: [3, 5, 10]
|
|
384
|
+
hybrid_weight: {min: 0.0, max: 1.0, step: 0.25}
|
|
385
|
+
reranker: [none, cross_encoder]
|
|
386
|
+
baseline: # optional; defaults to each knob's first value
|
|
387
|
+
chunk_size: 400
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`tune` writes the best config to `best_config.yaml`, which `evaluate
|
|
391
|
+
--config` accepts directly. Reading YAML uses PyYAML if installed
|
|
392
|
+
(`pip install "ragaudit-cli[yaml]"`) and a built-in parser for simple files
|
|
393
|
+
otherwise.
|
|
394
|
+
|
|
395
|
+
### CLI reference
|
|
396
|
+
|
|
397
|
+
| Command | Key options |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `ragaudit generate --corpus PATH` | `--n` (max facts per chunk, default 3), `--multi-hop`, `--multi-hop-pairs 10`, `--lexical-threshold 0.5`, `--max-chunks`, `--max-llm-calls`, `--generator-model`, `--verifier-model`, `--retry-processed` |
|
|
400
|
+
| `ragaudit evaluate --adapter FILE` | `--config YAML`, `--corpus PATH`, `--split dev\|test`, `--k 5`, `--regression-sample 50`, `--seed`, `--full`, `--ablation`, `--judge-model`, `--match-by id\|text` (text needs `--corpus`), `--match-threshold 0.5` |
|
|
401
|
+
| `ragaudit tune --adapter FILE --search-space YAML` | `--strategy grid\|random\|llm`, `--max-trials 20`, `--max-llm-calls`, `--seed`, `--k 5`, `--overfit-threshold 10`, `--out best_config.yaml`, `--judge-model`, `--proposer-model` |
|
|
402
|
+
| `ragaudit report --kind generate\|tune` | `--run-id` (generate run or tune id; default latest), `--out` |
|
|
403
|
+
|
|
404
|
+
Every v2 command takes `--store` (default `.ragaudit/store.db`, separate
|
|
405
|
+
from v1's `ragaudit.db`) and `--llm openai|mock` (or `RAGAUDIT_LLM`).
|
|
406
|
+
`--llm mock` runs everything offline with a deterministic mock that has
|
|
407
|
+
deliberate flaws, for demos and tests; its numbers say nothing about a
|
|
408
|
+
real pipeline. Without `--corpus`, `generate` is the unchanged v1 command,
|
|
409
|
+
and `report` defaults to the v1 run report.
|
|
410
|
+
|
|
411
|
+
### Known limitations
|
|
412
|
+
|
|
413
|
+
- The real-LLM path was smoke-tested on `generate` with local Ollama models
|
|
414
|
+
(qwen2.5:7b generator, llama3.1:8b verifier) and its bugs were fixed, but
|
|
415
|
+
a full real-LLM `evaluate`/`tune` run is still pending.
|
|
416
|
+
|
|
417
|
+
## Releasing
|
|
418
|
+
|
|
419
|
+
Pushing a tag `vX.Y.Z` that matches `version` in `pyproject.toml` runs
|
|
420
|
+
`.github/workflows/publish.yml`: tests, build, `twine check`, then upload
|
|
421
|
+
to PyPI through trusted publishing (no API token in GitHub). It needs a
|
|
422
|
+
one-time trusted publisher on pypi.org for project `ragaudit-cli`, this
|
|
423
|
+
repository, workflow `publish.yml`, environment `pypi`. (The distribution is
|
|
424
|
+
`ragaudit-cli` because PyPI treats `ragaudit` as the same name as the existing
|
|
425
|
+
`rag-audit` project.)
|
|
426
|
+
|
|
427
|
+
## License
|
|
428
|
+
|
|
429
|
+
MIT
|