retrace-rag 0.1.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 (51) hide show
  1. retrace_rag-0.1.0/.gitignore +21 -0
  2. retrace_rag-0.1.0/LICENSE +21 -0
  3. retrace_rag-0.1.0/PKG-INFO +254 -0
  4. retrace_rag-0.1.0/README.md +221 -0
  5. retrace_rag-0.1.0/docs/images/cli-before_after.png +0 -0
  6. retrace_rag-0.1.0/docs/images/cli-label.png +0 -0
  7. retrace_rag-0.1.0/docs/images/cli-run.png +0 -0
  8. retrace_rag-0.1.0/docs/images/cli-synth.png +0 -0
  9. retrace_rag-0.1.0/docs/images/report-overview.png +0 -0
  10. retrace_rag-0.1.0/docs/images/report-trace.png +0 -0
  11. retrace_rag-0.1.0/examples/before_after.py +140 -0
  12. retrace_rag-0.1.0/examples/build_sample_golden.py +180 -0
  13. retrace_rag-0.1.0/examples/quickstart.py +102 -0
  14. retrace_rag-0.1.0/pyproject.toml +91 -0
  15. retrace_rag-0.1.0/src/retrace/__init__.py +140 -0
  16. retrace_rag-0.1.0/src/retrace/attribution.py +378 -0
  17. retrace_rag-0.1.0/src/retrace/baseline.py +87 -0
  18. retrace_rag-0.1.0/src/retrace/chunking.py +246 -0
  19. retrace_rag-0.1.0/src/retrace/cli.py +454 -0
  20. retrace_rag-0.1.0/src/retrace/corpus.py +266 -0
  21. retrace_rag-0.1.0/src/retrace/errors.py +47 -0
  22. retrace_rag-0.1.0/src/retrace/fixtures.py +204 -0
  23. retrace_rag-0.1.0/src/retrace/judge.py +147 -0
  24. retrace_rag-0.1.0/src/retrace/labelling.py +435 -0
  25. retrace_rag-0.1.0/src/retrace/llm.py +627 -0
  26. retrace_rag-0.1.0/src/retrace/metrics.py +269 -0
  27. retrace_rag-0.1.0/src/retrace/protocols.py +175 -0
  28. retrace_rag-0.1.0/src/retrace/report.py +563 -0
  29. retrace_rag-0.1.0/src/retrace/retrievers.py +370 -0
  30. retrace_rag-0.1.0/src/retrace/runner.py +347 -0
  31. retrace_rag-0.1.0/src/retrace/sample_data/documents.json +9 -0
  32. retrace_rag-0.1.0/src/retrace/sample_data/golden.jsonl +12 -0
  33. retrace_rag-0.1.0/src/retrace/summary.py +121 -0
  34. retrace_rag-0.1.0/src/retrace/synthesis.py +602 -0
  35. retrace_rag-0.1.0/src/retrace/text.py +61 -0
  36. retrace_rag-0.1.0/src/retrace/types.py +251 -0
  37. retrace_rag-0.1.0/tests/test_attribution.py +227 -0
  38. retrace_rag-0.1.0/tests/test_baseline.py +202 -0
  39. retrace_rag-0.1.0/tests/test_cli.py +151 -0
  40. retrace_rag-0.1.0/tests/test_corpus.py +202 -0
  41. retrace_rag-0.1.0/tests/test_fixtures.py +165 -0
  42. retrace_rag-0.1.0/tests/test_judge.py +83 -0
  43. retrace_rag-0.1.0/tests/test_labelling.py +255 -0
  44. retrace_rag-0.1.0/tests/test_llm.py +168 -0
  45. retrace_rag-0.1.0/tests/test_metrics.py +326 -0
  46. retrace_rag-0.1.0/tests/test_openai_compatible.py +395 -0
  47. retrace_rag-0.1.0/tests/test_report.py +178 -0
  48. retrace_rag-0.1.0/tests/test_retrievers.py +292 -0
  49. retrace_rag-0.1.0/tests/test_runner.py +187 -0
  50. retrace_rag-0.1.0/tests/test_synthesis.py +371 -0
  51. retrace_rag-0.1.0/tests/test_types.py +147 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ # Corpora and generated artefacts are never committed.
14
+ data/ntrs/
15
+ data/cache/
16
+ runs/
17
+ *.report.html
18
+ .env
19
+
20
+ # Local editor / preview config
21
+ .claude/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Jaan
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,254 @@
1
+ Metadata-Version: 2.5
2
+ Name: retrace-rag
3
+ Version: 0.1.0
4
+ Summary: Find out which stage of your RAG pipeline is actually broken.
5
+ Project-URL: Homepage, https://github.com/mohammadjaan013/retrace
6
+ Project-URL: Source, https://github.com/mohammadjaan013/retrace
7
+ Project-URL: Issues, https://github.com/mohammadjaan013/retrace/issues
8
+ Author: Mohammad Jaan
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: diagnostics,evaluation,golden-set,llm,rag,retrieval
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Requires-Python: >=3.11
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=1.0; extra == 'anthropic'
26
+ Provides-Extra: dense
27
+ Requires-Dist: sentence-transformers>=3.0; extra == 'dense'
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Retrace
35
+
36
+ **Ragas tells you your RAG scores 0.72. Retrace tells you which questions failed, why, and what to fix first.**
37
+
38
+ Most RAG evaluation tools compute a score. Retrace answers the question the score cannot: *which stage of the pipeline is actually broken?* Every failed question gets exactly one verdict:
39
+
40
+ | Verdict | What happened | What you fix |
41
+ |---|---|---|
42
+ | `CORPUS_GAP` | The answer is not in the indexed documents at all. | Nothing in the pipeline. Add the sources, or scope the product down. |
43
+ | `RETRIEVAL_MISS` | The evidence is indexed but never retrieved. | Chunking, keyword search, dense retrieval, query rewriting. |
44
+ | `RANKING_MISS` | It was retrieved, then cut before the model saw it. | Reranking, context budget, fusion weights. |
45
+ | `GENERATION_MISS` | The model had the evidence and still answered wrong. | Prompt, context order, model choice. |
46
+ | `UNSCORABLE` | Retrace could not honestly decide. | Reported on its own line. Never folded into a pass rate. |
47
+
48
+ `CORPUS_GAP` is the verdict teams most need and rarely have. Weeks go into tuning embeddings for questions the documents cannot answer, and no aggregate score will tell you that is what is happening.
49
+
50
+ ---
51
+
52
+ ## Try it in 30 seconds
53
+
54
+ No API key, no downloads, no dependencies:
55
+
56
+ ```bash
57
+ pip install retrace-rag
58
+ retrace run --demo --report
59
+ ```
60
+
61
+ The demo uses a small sample corpus and test set that ship inside the package.
62
+
63
+ ```
64
+ VERDICTS 12 cases (12 trusted)
65
+ OK ############## 7 58%
66
+ CORPUS_GAP #### 2 17%
67
+ RETRIEVAL_MISS ###### 3 25%
68
+ RANKING_MISS not observable (tier 1 pipeline)
69
+ GENERATION_MISS not evaluated (no answers or no judge)
70
+
71
+ FIX FIRST
72
+ 1. RETRIEVAL_MISS - 3 of 5 failures (60%)
73
+ The evidence is indexed but retrieval never returns it.
74
+ 3 found by a reference keyword retriever (losing to BM25)
75
+ -> Check chunking first: answers split across a chunk boundary are unfindable.
76
+ 2. CORPUS_GAP - 2 of 5 failures (40%)
77
+ The answer is not in the indexed documents. Stop tuning retrieval for these.
78
+ ```
79
+
80
+ `--report` also writes `runs/run.html`: a single self-contained file with the verdict breakdown, the ranked fix list, and the full retrieval trace for every question, with the gold evidence highlighted. It has no external requests and no web fonts, so it can be emailed as is.
81
+
82
+ ### What it looks like
83
+
84
+ All screenshots are real output: the CLI captured from a terminal, the report rendered from an actual run.
85
+
86
+ | | |
87
+ |---|---|
88
+ | ![retrace run](https://raw.githubusercontent.com/mohammadjaan013/retrace/main/docs/images/cli-run.png) | ![HTML report](https://raw.githubusercontent.com/mohammadjaan013/retrace/main/docs/images/report-overview.png) |
89
+ | `retrace run`: verdicts, metrics and the ranked fix list | The HTML report |
90
+ | ![retrace label](https://raw.githubusercontent.com/mohammadjaan013/retrace/main/docs/images/cli-label.png) | ![A retrieval trace](https://raw.githubusercontent.com/mohammadjaan013/retrace/main/docs/images/report-trace.png) |
91
+ | `retrace label` reviewing a question generated from a NASA report | One failed question's trace, with the keyword probe's verdict |
92
+
93
+ ---
94
+
95
+ ## The workflow
96
+
97
+ ```bash
98
+ # 1. Propose candidate questions from your documents (needs an LLM -- see below)
99
+ retrace synth --docs docs.jsonl --out candidates.jsonl --n 60
100
+
101
+ # 2. Review them. One keypress each; every decision is saved as you go.
102
+ retrace label --candidates candidates.jsonl --docs docs.jsonl --out golden.jsonl
103
+
104
+ # 3. Diagnose your pipeline against the reviewed set
105
+ retrace run --pipeline my_rag:build() --docs docs.jsonl --fixture golden.jsonl --report
106
+ ```
107
+
108
+ **Step 1 is the one other tools skip.** They all assume you already have a labelled evaluation set, and almost nobody does. `synth` writes each question from a single passage and makes the model quote its evidence verbatim. Retrace then locates that quote and stores its exact character offsets, and any quote it cannot find is rejected as a hallucinated citation. Questions that copy their passage's wording are rejected too, because they make keyword retrieval look better than it is.
109
+
110
+ **Step 2 makes the set trustworthy.** Nothing a model writes counts as ground truth until a person accepts it. The labeller measures its own speed and tells you how long 50 labels take.
111
+
112
+ Documents are `.json` (`{"doc_id": "text", ...}`) or `.jsonl` (`{"id": ..., "text": ...}` per line).
113
+
114
+ ## Plugging in your pipeline
115
+
116
+ Point `--pipeline` at any `module:attribute`. It can resolve to a function, a class, or a factory call returning `(pipeline, corpus)`.
117
+
118
+ **Tier 1: a retrieve function.** About ten minutes of work.
119
+
120
+ ```python
121
+ # my_rag.py
122
+ from retrace import Chunk, Span
123
+
124
+ def retrieve(query: str, k: int) -> list[Chunk]:
125
+ hits = my_vector_store.search(query, top_k=k) # whatever you already have
126
+ return [
127
+ Chunk(id=h.id, doc_id=h.doc_id, text=h.text, score=h.score,
128
+ span=Span(h.doc_id, h.start_char, h.end_char)) # offsets make matching exact
129
+ for h in hits
130
+ ]
131
+ ```
132
+
133
+ **Tier 2: expose both stages.** Return a `RetrievalResult` with the candidates *before* reranking and the context *after*. This is the only way `RANKING_MISS` becomes observable, and it's the single most valuable thing to wire up.
134
+
135
+ ```python
136
+ from retrace import RetrievalResult
137
+
138
+ class MyPipeline:
139
+ def run(self, query: str) -> RetrievalResult:
140
+ candidates = retrieve(query, 50)
141
+ context = rerank(query, candidates)[:5]
142
+ return RetrievalResult(query, tuple(candidates), tuple(context),
143
+ answer=generate(query, context))
144
+ ```
145
+
146
+ Pass `--docs` (the documents your pipeline indexes) to enable `CORPUS_GAP` detection. Without them, a missing answer and a retrieval miss look identical, and Retrace says so rather than guessing.
147
+
148
+ ### Or from Python
149
+
150
+ ```python
151
+ from retrace import build_corpus, evaluate, load_documents, read_fixture
152
+ from retrace.report import write_report
153
+
154
+ corpus = build_corpus(load_documents("docs.jsonl"))
155
+ run = evaluate(my_pipeline, read_fixture("golden.jsonl"), corpus=corpus)
156
+ write_report(run, "report.html")
157
+ ```
158
+
159
+ ## Language models
160
+
161
+ Retrace needs a model for two optional jobs: writing candidate questions, and judging answers for `GENERATION_MISS`. The core defines a one-method protocol and depends on no provider.
162
+
163
+ Choose one with `--provider`. Keys are read from the environment, or from a `.env` file in the directory you run from, and are never printed.
164
+
165
+ | `--provider` | Key | Default model | Status |
166
+ |---|---|---|---|
167
+ | `groq` | `GROQ_API_KEY` | `openai/gpt-oss-120b` | Tested live; works on the free tier |
168
+ | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-flash` | Built from DeepSeek's docs; not yet tested live |
169
+ | `anthropic` | `ANTHROPIC_API_KEY` | `claude-opus-5` | Needs `pip install "retrace-rag[anthropic]"`; not yet tested live |
170
+ | `openai-compatible` | `--api-key-env` | `--model` | Any endpoint speaking the OpenAI protocol (OpenAI, Ollama, vLLM); untested |
171
+
172
+ ```bash
173
+ retrace synth --docs docs.jsonl --out candidates.jsonl --n 60 --provider groq --effort low
174
+ ```
175
+
176
+ Rate limits are waited out automatically, a passage the model fails on is skipped rather than stopping the run, and a stopped run keeps every question already written. For anything else, write a function `(system, prompt, schema) -> dict` and pass `--llm my_module:my_function`.
177
+
178
+ Answer judging defaults to a free token-recall judge, which is instant but blind to negation and paraphrase. Use `--judge llm` before putting a `GENERATION_MISS` number in front of anyone.
179
+
180
+ ## How a retrieval miss gets explained
181
+
182
+ A `RETRIEVAL_MISS` has two causes that need opposite fixes, so Retrace re-asks every missed question to a reference BM25 over your corpus:
183
+
184
+ - **The probe finds it** → *your pipeline is losing to plain keyword search.* Fix the lexical side: chunking, IDF, hybrid search.
185
+ - **The probe misses it too** → *a genuine vocabulary mismatch.* The user's words aren't the document's words, so this needs dense retrieval or query rewriting.
186
+
187
+ This probe replaced a word-overlap heuristic that turned out to be wrong. The heuristic called two of the demo's three misses vocabulary mismatches, and BM25 recovered all three. The naive baseline's real problem was stopword noise ("what", "is" and "the" matching every chunk), which counting shared words cannot see. `examples/before_after.py` runs the whole loop and says in its own output what the result does and doesn't prove.
188
+
189
+ ## Also included
190
+
191
+ Reference implementations of the fixes the verdicts point at. They are exact, in memory, and readable in one sitting, not a vector database.
192
+
193
+ - `BM25Retriever`: zero dependencies.
194
+ - `DenseRetriever`: bring any embedding function, or `pip install "retrace-rag[dense]"` for local sentence-transformers. Uses a content-addressed `EmbeddingCache`, so re-running an evaluation costs nothing.
195
+ - `HybridRetriever`: reciprocal rank fusion, recording which leg found each chunk (shown in the report's trace).
196
+ - `RerankingPipeline` + `CrossEncoderReranker`: a Tier 2 pipeline.
197
+ - `sentence_chunks`: whole sentences, paragraph-bounded, with overlap.
198
+
199
+ ## Demo corpus: 200 NASA technical reports
200
+
201
+ A real corpus for testing: 200 reports from the [NASA Technical Reports Server](https://ntrs.nasa.gov), 40 each on thermal control, power, attitude control, electric propulsion and deep-space communications, published 1962–2026. The document text isn't committed. `data/ntrs-manifest.json` pins every document by ID and hash, so anyone can rebuild the identical corpus:
202
+
203
+ ```bash
204
+ python scripts/ntrs.py fetch # ~10.7 MB, standard library only, rate-limited
205
+ ```
206
+
207
+ - **Only reports NASA marks as public-use-permitted.** Being hosted on a NASA server isn't the same as being free to reuse; 18 otherwise-matching reports were excluded on that basis.
208
+ - **NASA's own extracted text, not the PDFs.** Gold labels are character offsets. Extracting PDFs locally would make the text depend on the PDF library version and silently shift every offset on another machine.
209
+ - **Raw bytes and prepared text are both hash-pinned.** 15 of the 200 files came back as XHTML rather than plain text, so a deterministic conversion runs first, and every offset refers to its output.
210
+
211
+ It's realistically messy: OCR noise, tables broken one word per line, and sixty years of formatting drift. It also showed `sentence_chunks` a problem the toy corpus couldn't. In PDF-extracted text, 91% of blank lines are line wraps, not paragraph breaks. Treating them as boundaries produced 259,743 chunks with a median length of 7 characters. The chunker now detects this per document.
212
+
213
+ ## In CI
214
+
215
+ ```bash
216
+ retrace run --pipeline my_rag:build() --docs docs.jsonl --fixture golden.jsonl \
217
+ --max-failure-rate 0.25
218
+ ```
219
+
220
+ Exits `3` when the failure rate exceeds the threshold, and `2` on a usage or setup error, so a pipeline can tell "worse" from "broken". If the first cases all fail the same way (bad key, wrong URL, empty index), the run stops immediately instead of producing a report that is 100% `UNSCORABLE`.
221
+
222
+ ## Design decisions worth knowing
223
+
224
+ **Gold labels are character spans, not chunk ids.** Changing the chunking is the most common fix Retrace recommends, and chunk-id labels would be invalidated by exactly that fix. Spans into the source document survive any re-chunking. For the same reason, `.gitattributes` pins LF line endings: a CRLF checkout would shift every offset.
225
+
226
+ **It never emits a verdict the integration can't support.** No corpus means no `CORPUS_GAP`. A Tier 1 pipeline means no `RANKING_MISS`. No answers means generation is "not evaluated", never "passed". Every report says which of these applied.
227
+
228
+ **Corpus gaps are excluded from retrieval metrics**, and the excluded count is printed next to every metric. An answer that isn't indexed isn't a retrieval failure.
229
+
230
+ **It warns when a number is meaningless.** If `k` returns more than 10% of the corpus, recall is inflated and the run says so. That warning exists because the first baseline scored 100% recall by returning the entire corpus.
231
+
232
+ **Zero runtime dependencies**, checked in CI by installing into a bare environment and running the demo.
233
+
234
+ ## Limitations
235
+
236
+ - The sample corpus is a toy (4 documents), and the demo's numbers demonstrate the mechanism. They are not a benchmark.
237
+ - The Anthropic adapter is tested against a recorded request shape, not yet against the live API.
238
+ - The default answer judge can't detect negation. Treat its `GENERATION_MISS` counts as triage only.
239
+ - Reference retrievers are exact and in-memory, sized for evaluation runs, not production traffic.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ git clone https://github.com/mohammadjaan013/retrace.git
245
+ cd retrace
246
+ pip install -e ".[dev]"
247
+ python -m pytest
248
+ python -m ruff check .
249
+ python examples/before_after.py
250
+ ```
251
+
252
+ ## License
253
+
254
+ MIT
@@ -0,0 +1,221 @@
1
+ # Retrace
2
+
3
+ **Ragas tells you your RAG scores 0.72. Retrace tells you which questions failed, why, and what to fix first.**
4
+
5
+ Most RAG evaluation tools compute a score. Retrace answers the question the score cannot: *which stage of the pipeline is actually broken?* Every failed question gets exactly one verdict:
6
+
7
+ | Verdict | What happened | What you fix |
8
+ |---|---|---|
9
+ | `CORPUS_GAP` | The answer is not in the indexed documents at all. | Nothing in the pipeline. Add the sources, or scope the product down. |
10
+ | `RETRIEVAL_MISS` | The evidence is indexed but never retrieved. | Chunking, keyword search, dense retrieval, query rewriting. |
11
+ | `RANKING_MISS` | It was retrieved, then cut before the model saw it. | Reranking, context budget, fusion weights. |
12
+ | `GENERATION_MISS` | The model had the evidence and still answered wrong. | Prompt, context order, model choice. |
13
+ | `UNSCORABLE` | Retrace could not honestly decide. | Reported on its own line. Never folded into a pass rate. |
14
+
15
+ `CORPUS_GAP` is the verdict teams most need and rarely have. Weeks go into tuning embeddings for questions the documents cannot answer, and no aggregate score will tell you that is what is happening.
16
+
17
+ ---
18
+
19
+ ## Try it in 30 seconds
20
+
21
+ No API key, no downloads, no dependencies:
22
+
23
+ ```bash
24
+ pip install retrace-rag
25
+ retrace run --demo --report
26
+ ```
27
+
28
+ The demo uses a small sample corpus and test set that ship inside the package.
29
+
30
+ ```
31
+ VERDICTS 12 cases (12 trusted)
32
+ OK ############## 7 58%
33
+ CORPUS_GAP #### 2 17%
34
+ RETRIEVAL_MISS ###### 3 25%
35
+ RANKING_MISS not observable (tier 1 pipeline)
36
+ GENERATION_MISS not evaluated (no answers or no judge)
37
+
38
+ FIX FIRST
39
+ 1. RETRIEVAL_MISS - 3 of 5 failures (60%)
40
+ The evidence is indexed but retrieval never returns it.
41
+ 3 found by a reference keyword retriever (losing to BM25)
42
+ -> Check chunking first: answers split across a chunk boundary are unfindable.
43
+ 2. CORPUS_GAP - 2 of 5 failures (40%)
44
+ The answer is not in the indexed documents. Stop tuning retrieval for these.
45
+ ```
46
+
47
+ `--report` also writes `runs/run.html`: a single self-contained file with the verdict breakdown, the ranked fix list, and the full retrieval trace for every question, with the gold evidence highlighted. It has no external requests and no web fonts, so it can be emailed as is.
48
+
49
+ ### What it looks like
50
+
51
+ All screenshots are real output: the CLI captured from a terminal, the report rendered from an actual run.
52
+
53
+ | | |
54
+ |---|---|
55
+ | ![retrace run](docs/images/cli-run.png) | ![HTML report](docs/images/report-overview.png) |
56
+ | `retrace run`: verdicts, metrics and the ranked fix list | The HTML report |
57
+ | ![retrace label](docs/images/cli-label.png) | ![A retrieval trace](docs/images/report-trace.png) |
58
+ | `retrace label` reviewing a question generated from a NASA report | One failed question's trace, with the keyword probe's verdict |
59
+
60
+ ---
61
+
62
+ ## The workflow
63
+
64
+ ```bash
65
+ # 1. Propose candidate questions from your documents (needs an LLM -- see below)
66
+ retrace synth --docs docs.jsonl --out candidates.jsonl --n 60
67
+
68
+ # 2. Review them. One keypress each; every decision is saved as you go.
69
+ retrace label --candidates candidates.jsonl --docs docs.jsonl --out golden.jsonl
70
+
71
+ # 3. Diagnose your pipeline against the reviewed set
72
+ retrace run --pipeline my_rag:build() --docs docs.jsonl --fixture golden.jsonl --report
73
+ ```
74
+
75
+ **Step 1 is the one other tools skip.** They all assume you already have a labelled evaluation set, and almost nobody does. `synth` writes each question from a single passage and makes the model quote its evidence verbatim. Retrace then locates that quote and stores its exact character offsets, and any quote it cannot find is rejected as a hallucinated citation. Questions that copy their passage's wording are rejected too, because they make keyword retrieval look better than it is.
76
+
77
+ **Step 2 makes the set trustworthy.** Nothing a model writes counts as ground truth until a person accepts it. The labeller measures its own speed and tells you how long 50 labels take.
78
+
79
+ Documents are `.json` (`{"doc_id": "text", ...}`) or `.jsonl` (`{"id": ..., "text": ...}` per line).
80
+
81
+ ## Plugging in your pipeline
82
+
83
+ Point `--pipeline` at any `module:attribute`. It can resolve to a function, a class, or a factory call returning `(pipeline, corpus)`.
84
+
85
+ **Tier 1: a retrieve function.** About ten minutes of work.
86
+
87
+ ```python
88
+ # my_rag.py
89
+ from retrace import Chunk, Span
90
+
91
+ def retrieve(query: str, k: int) -> list[Chunk]:
92
+ hits = my_vector_store.search(query, top_k=k) # whatever you already have
93
+ return [
94
+ Chunk(id=h.id, doc_id=h.doc_id, text=h.text, score=h.score,
95
+ span=Span(h.doc_id, h.start_char, h.end_char)) # offsets make matching exact
96
+ for h in hits
97
+ ]
98
+ ```
99
+
100
+ **Tier 2: expose both stages.** Return a `RetrievalResult` with the candidates *before* reranking and the context *after*. This is the only way `RANKING_MISS` becomes observable, and it's the single most valuable thing to wire up.
101
+
102
+ ```python
103
+ from retrace import RetrievalResult
104
+
105
+ class MyPipeline:
106
+ def run(self, query: str) -> RetrievalResult:
107
+ candidates = retrieve(query, 50)
108
+ context = rerank(query, candidates)[:5]
109
+ return RetrievalResult(query, tuple(candidates), tuple(context),
110
+ answer=generate(query, context))
111
+ ```
112
+
113
+ Pass `--docs` (the documents your pipeline indexes) to enable `CORPUS_GAP` detection. Without them, a missing answer and a retrieval miss look identical, and Retrace says so rather than guessing.
114
+
115
+ ### Or from Python
116
+
117
+ ```python
118
+ from retrace import build_corpus, evaluate, load_documents, read_fixture
119
+ from retrace.report import write_report
120
+
121
+ corpus = build_corpus(load_documents("docs.jsonl"))
122
+ run = evaluate(my_pipeline, read_fixture("golden.jsonl"), corpus=corpus)
123
+ write_report(run, "report.html")
124
+ ```
125
+
126
+ ## Language models
127
+
128
+ Retrace needs a model for two optional jobs: writing candidate questions, and judging answers for `GENERATION_MISS`. The core defines a one-method protocol and depends on no provider.
129
+
130
+ Choose one with `--provider`. Keys are read from the environment, or from a `.env` file in the directory you run from, and are never printed.
131
+
132
+ | `--provider` | Key | Default model | Status |
133
+ |---|---|---|---|
134
+ | `groq` | `GROQ_API_KEY` | `openai/gpt-oss-120b` | Tested live; works on the free tier |
135
+ | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-flash` | Built from DeepSeek's docs; not yet tested live |
136
+ | `anthropic` | `ANTHROPIC_API_KEY` | `claude-opus-5` | Needs `pip install "retrace-rag[anthropic]"`; not yet tested live |
137
+ | `openai-compatible` | `--api-key-env` | `--model` | Any endpoint speaking the OpenAI protocol (OpenAI, Ollama, vLLM); untested |
138
+
139
+ ```bash
140
+ retrace synth --docs docs.jsonl --out candidates.jsonl --n 60 --provider groq --effort low
141
+ ```
142
+
143
+ Rate limits are waited out automatically, a passage the model fails on is skipped rather than stopping the run, and a stopped run keeps every question already written. For anything else, write a function `(system, prompt, schema) -> dict` and pass `--llm my_module:my_function`.
144
+
145
+ Answer judging defaults to a free token-recall judge, which is instant but blind to negation and paraphrase. Use `--judge llm` before putting a `GENERATION_MISS` number in front of anyone.
146
+
147
+ ## How a retrieval miss gets explained
148
+
149
+ A `RETRIEVAL_MISS` has two causes that need opposite fixes, so Retrace re-asks every missed question to a reference BM25 over your corpus:
150
+
151
+ - **The probe finds it** → *your pipeline is losing to plain keyword search.* Fix the lexical side: chunking, IDF, hybrid search.
152
+ - **The probe misses it too** → *a genuine vocabulary mismatch.* The user's words aren't the document's words, so this needs dense retrieval or query rewriting.
153
+
154
+ This probe replaced a word-overlap heuristic that turned out to be wrong. The heuristic called two of the demo's three misses vocabulary mismatches, and BM25 recovered all three. The naive baseline's real problem was stopword noise ("what", "is" and "the" matching every chunk), which counting shared words cannot see. `examples/before_after.py` runs the whole loop and says in its own output what the result does and doesn't prove.
155
+
156
+ ## Also included
157
+
158
+ Reference implementations of the fixes the verdicts point at. They are exact, in memory, and readable in one sitting, not a vector database.
159
+
160
+ - `BM25Retriever`: zero dependencies.
161
+ - `DenseRetriever`: bring any embedding function, or `pip install "retrace-rag[dense]"` for local sentence-transformers. Uses a content-addressed `EmbeddingCache`, so re-running an evaluation costs nothing.
162
+ - `HybridRetriever`: reciprocal rank fusion, recording which leg found each chunk (shown in the report's trace).
163
+ - `RerankingPipeline` + `CrossEncoderReranker`: a Tier 2 pipeline.
164
+ - `sentence_chunks`: whole sentences, paragraph-bounded, with overlap.
165
+
166
+ ## Demo corpus: 200 NASA technical reports
167
+
168
+ A real corpus for testing: 200 reports from the [NASA Technical Reports Server](https://ntrs.nasa.gov), 40 each on thermal control, power, attitude control, electric propulsion and deep-space communications, published 1962–2026. The document text isn't committed. `data/ntrs-manifest.json` pins every document by ID and hash, so anyone can rebuild the identical corpus:
169
+
170
+ ```bash
171
+ python scripts/ntrs.py fetch # ~10.7 MB, standard library only, rate-limited
172
+ ```
173
+
174
+ - **Only reports NASA marks as public-use-permitted.** Being hosted on a NASA server isn't the same as being free to reuse; 18 otherwise-matching reports were excluded on that basis.
175
+ - **NASA's own extracted text, not the PDFs.** Gold labels are character offsets. Extracting PDFs locally would make the text depend on the PDF library version and silently shift every offset on another machine.
176
+ - **Raw bytes and prepared text are both hash-pinned.** 15 of the 200 files came back as XHTML rather than plain text, so a deterministic conversion runs first, and every offset refers to its output.
177
+
178
+ It's realistically messy: OCR noise, tables broken one word per line, and sixty years of formatting drift. It also showed `sentence_chunks` a problem the toy corpus couldn't. In PDF-extracted text, 91% of blank lines are line wraps, not paragraph breaks. Treating them as boundaries produced 259,743 chunks with a median length of 7 characters. The chunker now detects this per document.
179
+
180
+ ## In CI
181
+
182
+ ```bash
183
+ retrace run --pipeline my_rag:build() --docs docs.jsonl --fixture golden.jsonl \
184
+ --max-failure-rate 0.25
185
+ ```
186
+
187
+ Exits `3` when the failure rate exceeds the threshold, and `2` on a usage or setup error, so a pipeline can tell "worse" from "broken". If the first cases all fail the same way (bad key, wrong URL, empty index), the run stops immediately instead of producing a report that is 100% `UNSCORABLE`.
188
+
189
+ ## Design decisions worth knowing
190
+
191
+ **Gold labels are character spans, not chunk ids.** Changing the chunking is the most common fix Retrace recommends, and chunk-id labels would be invalidated by exactly that fix. Spans into the source document survive any re-chunking. For the same reason, `.gitattributes` pins LF line endings: a CRLF checkout would shift every offset.
192
+
193
+ **It never emits a verdict the integration can't support.** No corpus means no `CORPUS_GAP`. A Tier 1 pipeline means no `RANKING_MISS`. No answers means generation is "not evaluated", never "passed". Every report says which of these applied.
194
+
195
+ **Corpus gaps are excluded from retrieval metrics**, and the excluded count is printed next to every metric. An answer that isn't indexed isn't a retrieval failure.
196
+
197
+ **It warns when a number is meaningless.** If `k` returns more than 10% of the corpus, recall is inflated and the run says so. That warning exists because the first baseline scored 100% recall by returning the entire corpus.
198
+
199
+ **Zero runtime dependencies**, checked in CI by installing into a bare environment and running the demo.
200
+
201
+ ## Limitations
202
+
203
+ - The sample corpus is a toy (4 documents), and the demo's numbers demonstrate the mechanism. They are not a benchmark.
204
+ - The Anthropic adapter is tested against a recorded request shape, not yet against the live API.
205
+ - The default answer judge can't detect negation. Treat its `GENERATION_MISS` counts as triage only.
206
+ - Reference retrievers are exact and in-memory, sized for evaluation runs, not production traffic.
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ git clone https://github.com/mohammadjaan013/retrace.git
212
+ cd retrace
213
+ pip install -e ".[dev]"
214
+ python -m pytest
215
+ python -m ruff check .
216
+ python examples/before_after.py
217
+ ```
218
+
219
+ ## License
220
+
221
+ MIT