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.
- retrace_rag-0.1.0/.gitignore +21 -0
- retrace_rag-0.1.0/LICENSE +21 -0
- retrace_rag-0.1.0/PKG-INFO +254 -0
- retrace_rag-0.1.0/README.md +221 -0
- retrace_rag-0.1.0/docs/images/cli-before_after.png +0 -0
- retrace_rag-0.1.0/docs/images/cli-label.png +0 -0
- retrace_rag-0.1.0/docs/images/cli-run.png +0 -0
- retrace_rag-0.1.0/docs/images/cli-synth.png +0 -0
- retrace_rag-0.1.0/docs/images/report-overview.png +0 -0
- retrace_rag-0.1.0/docs/images/report-trace.png +0 -0
- retrace_rag-0.1.0/examples/before_after.py +140 -0
- retrace_rag-0.1.0/examples/build_sample_golden.py +180 -0
- retrace_rag-0.1.0/examples/quickstart.py +102 -0
- retrace_rag-0.1.0/pyproject.toml +91 -0
- retrace_rag-0.1.0/src/retrace/__init__.py +140 -0
- retrace_rag-0.1.0/src/retrace/attribution.py +378 -0
- retrace_rag-0.1.0/src/retrace/baseline.py +87 -0
- retrace_rag-0.1.0/src/retrace/chunking.py +246 -0
- retrace_rag-0.1.0/src/retrace/cli.py +454 -0
- retrace_rag-0.1.0/src/retrace/corpus.py +266 -0
- retrace_rag-0.1.0/src/retrace/errors.py +47 -0
- retrace_rag-0.1.0/src/retrace/fixtures.py +204 -0
- retrace_rag-0.1.0/src/retrace/judge.py +147 -0
- retrace_rag-0.1.0/src/retrace/labelling.py +435 -0
- retrace_rag-0.1.0/src/retrace/llm.py +627 -0
- retrace_rag-0.1.0/src/retrace/metrics.py +269 -0
- retrace_rag-0.1.0/src/retrace/protocols.py +175 -0
- retrace_rag-0.1.0/src/retrace/report.py +563 -0
- retrace_rag-0.1.0/src/retrace/retrievers.py +370 -0
- retrace_rag-0.1.0/src/retrace/runner.py +347 -0
- retrace_rag-0.1.0/src/retrace/sample_data/documents.json +9 -0
- retrace_rag-0.1.0/src/retrace/sample_data/golden.jsonl +12 -0
- retrace_rag-0.1.0/src/retrace/summary.py +121 -0
- retrace_rag-0.1.0/src/retrace/synthesis.py +602 -0
- retrace_rag-0.1.0/src/retrace/text.py +61 -0
- retrace_rag-0.1.0/src/retrace/types.py +251 -0
- retrace_rag-0.1.0/tests/test_attribution.py +227 -0
- retrace_rag-0.1.0/tests/test_baseline.py +202 -0
- retrace_rag-0.1.0/tests/test_cli.py +151 -0
- retrace_rag-0.1.0/tests/test_corpus.py +202 -0
- retrace_rag-0.1.0/tests/test_fixtures.py +165 -0
- retrace_rag-0.1.0/tests/test_judge.py +83 -0
- retrace_rag-0.1.0/tests/test_labelling.py +255 -0
- retrace_rag-0.1.0/tests/test_llm.py +168 -0
- retrace_rag-0.1.0/tests/test_metrics.py +326 -0
- retrace_rag-0.1.0/tests/test_openai_compatible.py +395 -0
- retrace_rag-0.1.0/tests/test_report.py +178 -0
- retrace_rag-0.1.0/tests/test_retrievers.py +292 -0
- retrace_rag-0.1.0/tests/test_runner.py +187 -0
- retrace_rag-0.1.0/tests/test_synthesis.py +371 -0
- 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
|
+
|  |  |
|
|
89
|
+
| `retrace run`: verdicts, metrics and the ranked fix list | The HTML report |
|
|
90
|
+
|  |  |
|
|
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
|
+
|  |  |
|
|
56
|
+
| `retrace run`: verdicts, metrics and the ranked fix list | The HTML report |
|
|
57
|
+
|  |  |
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|