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.
Files changed (105) hide show
  1. ragaudit_cli-0.3.0/.gitignore +22 -0
  2. ragaudit_cli-0.3.0/CHANGELOG.md +99 -0
  3. ragaudit_cli-0.3.0/LICENSE +21 -0
  4. ragaudit_cli-0.3.0/PKG-INFO +429 -0
  5. ragaudit_cli-0.3.0/README.md +385 -0
  6. ragaudit_cli-0.3.0/pyproject.toml +79 -0
  7. ragaudit_cli-0.3.0/src/ragaudit/__init__.py +1 -0
  8. ragaudit_cli-0.3.0/src/ragaudit/adapters/__init__.py +0 -0
  9. ragaudit_cli-0.3.0/src/ragaudit/adapters/base.py +109 -0
  10. ragaudit_cli-0.3.0/src/ragaudit/adapters/callable_adapter.py +15 -0
  11. ragaudit_cli-0.3.0/src/ragaudit/cli.py +540 -0
  12. ragaudit_cli-0.3.0/src/ragaudit/cli_easy.py +458 -0
  13. ragaudit_cli-0.3.0/src/ragaudit/cli_v2.py +507 -0
  14. ragaudit_cli-0.3.0/src/ragaudit/env.py +46 -0
  15. ragaudit_cli-0.3.0/src/ragaudit/generate/__init__.py +0 -0
  16. ragaudit_cli-0.3.0/src/ragaudit/generate/pipeline.py +486 -0
  17. ragaudit_cli-0.3.0/src/ragaudit/generate/prompts.py +116 -0
  18. ragaudit_cli-0.3.0/src/ragaudit/generate/testset_generator.py +146 -0
  19. ragaudit_cli-0.3.0/src/ragaudit/generate/text.py +75 -0
  20. ragaudit_cli-0.3.0/src/ragaudit/ingest/__init__.py +0 -0
  21. ragaudit_cli-0.3.0/src/ragaudit/ingest/chunker.py +53 -0
  22. ragaudit_cli-0.3.0/src/ragaudit/ingest/corpus.py +120 -0
  23. ragaudit_cli-0.3.0/src/ragaudit/ingest/loader.py +49 -0
  24. ragaudit_cli-0.3.0/src/ragaudit/ingest/versioning.py +92 -0
  25. ragaudit_cli-0.3.0/src/ragaudit/llm/__init__.py +0 -0
  26. ragaudit_cli-0.3.0/src/ragaudit/llm/client.py +86 -0
  27. ragaudit_cli-0.3.0/src/ragaudit/llm/factory.py +70 -0
  28. ragaudit_cli-0.3.0/src/ragaudit/llm/json_output.py +72 -0
  29. ragaudit_cli-0.3.0/src/ragaudit/llm/mock.py +552 -0
  30. ragaudit_cli-0.3.0/src/ragaudit/models/__init__.py +0 -0
  31. ragaudit_cli-0.3.0/src/ragaudit/models/adapter.py +39 -0
  32. ragaudit_cli-0.3.0/src/ragaudit/models/document.py +28 -0
  33. ragaudit_cli-0.3.0/src/ragaudit/models/evaluation.py +70 -0
  34. ragaudit_cli-0.3.0/src/ragaudit/models/question.py +152 -0
  35. ragaudit_cli-0.3.0/src/ragaudit/models/run.py +44 -0
  36. ragaudit_cli-0.3.0/src/ragaudit/models/scoring.py +82 -0
  37. ragaudit_cli-0.3.0/src/ragaudit/models/testset.py +30 -0
  38. ragaudit_cli-0.3.0/src/ragaudit/models/tune.py +56 -0
  39. ragaudit_cli-0.3.0/src/ragaudit/report/__init__.py +0 -0
  40. ragaudit_cli-0.3.0/src/ragaudit/report/generator.py +155 -0
  41. ragaudit_cli-0.3.0/src/ragaudit/report/plain_summary.py +122 -0
  42. ragaudit_cli-0.3.0/src/ragaudit/report/templates/_v2_tooltip.jinja +43 -0
  43. ragaudit_cli-0.3.0/src/ragaudit/report/templates/generate_report.html.jinja +175 -0
  44. ragaudit_cli-0.3.0/src/ragaudit/report/templates/report.html.jinja +252 -0
  45. ragaudit_cli-0.3.0/src/ragaudit/report/templates/style.css +1095 -0
  46. ragaudit_cli-0.3.0/src/ragaudit/report/templates/tune_report.html.jinja +158 -0
  47. ragaudit_cli-0.3.0/src/ragaudit/report/templates/v2.css +286 -0
  48. ragaudit_cli-0.3.0/src/ragaudit/report/v2.py +380 -0
  49. ragaudit_cli-0.3.0/src/ragaudit/runner/__init__.py +0 -0
  50. ragaudit_cli-0.3.0/src/ragaudit/runner/ablation.py +46 -0
  51. ragaudit_cli-0.3.0/src/ragaudit/runner/evaluate.py +325 -0
  52. ragaudit_cli-0.3.0/src/ragaudit/runner/executor.py +98 -0
  53. ragaudit_cli-0.3.0/src/ragaudit/runner/locked.py +37 -0
  54. ragaudit_cli-0.3.0/src/ragaudit/runner/summary.py +45 -0
  55. ragaudit_cli-0.3.0/src/ragaudit/scaffold.py +252 -0
  56. ragaudit_cli-0.3.0/src/ragaudit/scoring/__init__.py +0 -0
  57. ragaudit_cli-0.3.0/src/ragaudit/scoring/correctness.py +94 -0
  58. ragaudit_cli-0.3.0/src/ragaudit/scoring/groundedness.py +86 -0
  59. ragaudit_cli-0.3.0/src/ragaudit/scoring/retrieval.py +42 -0
  60. ragaudit_cli-0.3.0/src/ragaudit/scoring/text_match.py +51 -0
  61. ragaudit_cli-0.3.0/src/ragaudit/scoring/verdict.py +14 -0
  62. ragaudit_cli-0.3.0/src/ragaudit/storage/__init__.py +0 -0
  63. ragaudit_cli-0.3.0/src/ragaudit/storage/db.py +81 -0
  64. ragaudit_cli-0.3.0/src/ragaudit/storage/lock.py +36 -0
  65. ragaudit_cli-0.3.0/src/ragaudit/storage/repository.py +216 -0
  66. ragaudit_cli-0.3.0/src/ragaudit/storage/store.py +536 -0
  67. ragaudit_cli-0.3.0/src/ragaudit/tune/__init__.py +7 -0
  68. ragaudit_cli-0.3.0/src/ragaudit/tune/devset.py +16 -0
  69. ragaudit_cli-0.3.0/src/ragaudit/tune/loop.py +208 -0
  70. ragaudit_cli-0.3.0/src/ragaudit/tune/search_space.py +140 -0
  71. ragaudit_cli-0.3.0/src/ragaudit/tune/strategies.py +197 -0
  72. ragaudit_cli-0.3.0/src/ragaudit/yaml_io.py +199 -0
  73. ragaudit_cli-0.3.0/tests/conftest.py +7 -0
  74. ragaudit_cli-0.3.0/tests/fixtures/adapter_raises_on_import.py +1 -0
  75. ragaudit_cli-0.3.0/tests/fixtures/adapter_with_neither.py +2 -0
  76. ragaudit_cli-0.3.0/tests/fixtures/adapter_with_object.py +9 -0
  77. ragaudit_cli-0.3.0/tests/fixtures/adapter_with_query.py +5 -0
  78. ragaudit_cli-0.3.0/tests/test_ablation.py +97 -0
  79. ragaudit_cli-0.3.0/tests/test_adapter_loading.py +37 -0
  80. ragaudit_cli-0.3.0/tests/test_check.py +269 -0
  81. ragaudit_cli-0.3.0/tests/test_chunker.py +44 -0
  82. ragaudit_cli-0.3.0/tests/test_cli.py +168 -0
  83. ragaudit_cli-0.3.0/tests/test_correctness.py +54 -0
  84. ragaudit_cli-0.3.0/tests/test_demo_adapter.py +127 -0
  85. ragaudit_cli-0.3.0/tests/test_doctor.py +196 -0
  86. ragaudit_cli-0.3.0/tests/test_executor.py +118 -0
  87. ragaudit_cli-0.3.0/tests/test_groundedness.py +40 -0
  88. ragaudit_cli-0.3.0/tests/test_init.py +159 -0
  89. ragaudit_cli-0.3.0/tests/test_llm_client.py +81 -0
  90. ragaudit_cli-0.3.0/tests/test_match_by_text.py +226 -0
  91. ragaudit_cli-0.3.0/tests/test_report_generator.py +361 -0
  92. ragaudit_cli-0.3.0/tests/test_repository.py +163 -0
  93. ragaudit_cli-0.3.0/tests/test_retrieval.py +37 -0
  94. ragaudit_cli-0.3.0/tests/test_summary.py +143 -0
  95. ragaudit_cli-0.3.0/tests/test_testset_generator.py +152 -0
  96. ragaudit_cli-0.3.0/tests/test_v2_evaluate.py +302 -0
  97. ragaudit_cli-0.3.0/tests/test_v2_generate.py +386 -0
  98. ragaudit_cli-0.3.0/tests/test_v2_llm_output.py +176 -0
  99. ragaudit_cli-0.3.0/tests/test_v2_locked_test_set.py +81 -0
  100. ragaudit_cli-0.3.0/tests/test_v2_reports.py +246 -0
  101. ragaudit_cli-0.3.0/tests/test_v2_split.py +256 -0
  102. ragaudit_cli-0.3.0/tests/test_v2_store.py +172 -0
  103. ragaudit_cli-0.3.0/tests/test_v2_tune.py +346 -0
  104. ragaudit_cli-0.3.0/tests/test_v2_versioning.py +118 -0
  105. 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