contexteval 0.2.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.
- contexteval-0.2.0/CHANGELOG.md +36 -0
- contexteval-0.2.0/LICENSE +21 -0
- contexteval-0.2.0/MANIFEST.in +2 -0
- contexteval-0.2.0/PKG-INFO +307 -0
- contexteval-0.2.0/README.md +263 -0
- contexteval-0.2.0/contexteval/__init__.py +19 -0
- contexteval-0.2.0/contexteval/__main__.py +7 -0
- contexteval-0.2.0/contexteval/_capture.py +821 -0
- contexteval-0.2.0/contexteval/_cli.py +477 -0
- contexteval-0.2.0/contexteval/_diagnose.py +347 -0
- contexteval-0.2.0/contexteval/_hooks.py +439 -0
- contexteval-0.2.0/contexteval/_intervention.py +203 -0
- contexteval-0.2.0/contexteval/_llm.py +248 -0
- contexteval-0.2.0/contexteval/_runner.py +196 -0
- contexteval-0.2.0/contexteval/_store.py +489 -0
- contexteval-0.2.0/contexteval/_testset.py +191 -0
- contexteval-0.2.0/contexteval/_text.py +124 -0
- contexteval-0.2.0/contexteval/_ui.py +309 -0
- contexteval-0.2.0/contexteval/demo/__init__.py +1 -0
- contexteval-0.2.0/contexteval/demo/_lock.py +39 -0
- contexteval-0.2.0/contexteval/demo/app.py +169 -0
- contexteval-0.2.0/contexteval/demo/corpus/company.md +11 -0
- contexteval-0.2.0/contexteval/demo/corpus/returns.md +11 -0
- contexteval-0.2.0/contexteval/demo/corpus/shipping.md +13 -0
- contexteval-0.2.0/contexteval/demo/corpus/warranty.md +15 -0
- contexteval-0.2.0/contexteval/demo/fake_llm.py +161 -0
- contexteval-0.2.0/contexteval/demo/planted.py +132 -0
- contexteval-0.2.0/contexteval/types.py +364 -0
- contexteval-0.2.0/contexteval/ui.html +433 -0
- contexteval-0.2.0/contexteval.egg-info/PKG-INFO +307 -0
- contexteval-0.2.0/contexteval.egg-info/SOURCES.txt +52 -0
- contexteval-0.2.0/contexteval.egg-info/dependency_links.txt +1 -0
- contexteval-0.2.0/contexteval.egg-info/entry_points.txt +2 -0
- contexteval-0.2.0/contexteval.egg-info/requires.txt +23 -0
- contexteval-0.2.0/contexteval.egg-info/top_level.txt +1 -0
- contexteval-0.2.0/pyproject.toml +56 -0
- contexteval-0.2.0/setup.cfg +4 -0
- contexteval-0.2.0/tests/conftest.py +73 -0
- contexteval-0.2.0/tests/test_acceptance.py +66 -0
- contexteval-0.2.0/tests/test_capture.py +351 -0
- contexteval-0.2.0/tests/test_cli.py +257 -0
- contexteval-0.2.0/tests/test_demo.py +197 -0
- contexteval-0.2.0/tests/test_diagnose.py +319 -0
- contexteval-0.2.0/tests/test_diagnose_table.py +231 -0
- contexteval-0.2.0/tests/test_experiment.py +194 -0
- contexteval-0.2.0/tests/test_hooks.py +696 -0
- contexteval-0.2.0/tests/test_intervention.py +135 -0
- contexteval-0.2.0/tests/test_llm.py +49 -0
- contexteval-0.2.0/tests/test_retriever.py +282 -0
- contexteval-0.2.0/tests/test_runner.py +199 -0
- contexteval-0.2.0/tests/test_store.py +245 -0
- contexteval-0.2.0/tests/test_testset.py +131 -0
- contexteval-0.2.0/tests/test_text.py +131 -0
- contexteval-0.2.0/tests/test_ui.py +551 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows [Semantic Versioning](https://semver.org/). Each release lists what changed for people who use contexteval.
|
|
4
|
+
|
|
5
|
+
## 0.2.0 (v1.1, 2026-10-11)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Diagnosis replays take a majority over 3 samples per step by default. Set the count with `contexteval diagnose --samples N` (odd, at least 1). A step stops sampling once a majority is decided, so a single lucky pass no longer decides a label.
|
|
10
|
+
- Stored diagnoses record the pass count of each replay step. The CLI and the dashboard show them, for example "gold to top 2/3".
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Async apps work. contexteval hooks `AsyncOpenAI` chat completions and responses, `AsyncAnthropic` messages, and chromadb's async collection query. `async def` functions work under `@contexteval.retriever`, and `contexteval run` accepts an `async def answer` entry point.
|
|
15
|
+
- Interventions and experiment settings apply to async retrievals exactly as to sync ones.
|
|
16
|
+
- Production capture with `contexteval.init()` works per asyncio task.
|
|
17
|
+
- Published on PyPI as `contexteval`, with complete package metadata.
|
|
18
|
+
- A GitHub Actions workflow runs the test suite on Python 3.10 to 3.13 on every push and pull request.
|
|
19
|
+
- A release workflow builds the package and publishes it to PyPI when a `v*` tag is pushed, through PyPI trusted publishing. No API token is stored in the repository.
|
|
20
|
+
|
|
21
|
+
## 0.1.0 (v1, not published to PyPI)
|
|
22
|
+
|
|
23
|
+
The first working version, installed from a checkout.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `contexteval init`, `build-tests`, `run`, `diagnose`, `experiment`, and `ui` commands.
|
|
28
|
+
- Capture without app changes: hooks for chromadb `Collection.query`, pinecone `Index.query`, OpenAI chat completions and responses, and Anthropic messages. `@contexteval.retriever` marks any other retrieval function.
|
|
29
|
+
- A test set builder that generates question, answer, and evidence cases from `.md` and `.txt` documents, and keeps only cases whose evidence appears word for word. JSONL and CSV import for your own cases.
|
|
30
|
+
- Grading by normalized substring match, with an LLM judge as fallback.
|
|
31
|
+
- Diagnosis that labels each failure `missing` (`not_retrieved` or `dropped`), `irrelevant`, `ignored`, `not_context`, or `flaky` by replaying the case with changed context. Cases without evidence and production traces go to an LLM judge.
|
|
32
|
+
- Experiments over `top_k`, `rerank`, `order`, `max_chunks`, and `compress`, with a baseline row and a comparison table.
|
|
33
|
+
- A local dashboard bound to `127.0.0.1`.
|
|
34
|
+
- Production capture through `contexteval.init()`, stored as ungraded traces.
|
|
35
|
+
- A bundled demo app with planted failures of every label, runnable offline with `--provider fake`.
|
|
36
|
+
- Local SQLite storage, a cost estimate before every command that calls an LLM, and no telemetry.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aakarshan-coding
|
|
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,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contexteval
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Find out why your RAG app's answer failed: label each failure as missing, irrelevant, or ignored context, then test the fix.
|
|
5
|
+
Author: aakarshan-coding
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aakarshan-coding/context-evaluation-sdk
|
|
8
|
+
Project-URL: Source, https://github.com/aakarshan-coding/context-evaluation-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/aakarshan-coding/context-evaluation-sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/aakarshan-coding/context-evaluation-sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: rag,llm,evaluation,retrieval,context,diagnosis,openai,anthropic,chromadb,pinecone
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
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.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: openai>=1.40; extra == "openai"
|
|
28
|
+
Provides-Extra: anthropic
|
|
29
|
+
Requires-Dist: anthropic>=0.34; extra == "anthropic"
|
|
30
|
+
Provides-Extra: chroma
|
|
31
|
+
Requires-Dist: chromadb>=0.5; extra == "chroma"
|
|
32
|
+
Provides-Extra: pinecone
|
|
33
|
+
Requires-Dist: pinecone>=5; extra == "pinecone"
|
|
34
|
+
Provides-Extra: demo
|
|
35
|
+
Requires-Dist: chromadb>=0.5; extra == "demo"
|
|
36
|
+
Requires-Dist: openai>=1.40; extra == "demo"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
39
|
+
Requires-Dist: chromadb>=0.5; extra == "dev"
|
|
40
|
+
Requires-Dist: openai>=1.40; extra == "dev"
|
|
41
|
+
Requires-Dist: anthropic>=0.34; extra == "dev"
|
|
42
|
+
Requires-Dist: httpx; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# contexteval
|
|
46
|
+
|
|
47
|
+
contexteval tells you why your RAG app gave a wrong answer. It runs your app on test questions, records what was retrieved and what reached the prompt, and labels each failure.
|
|
48
|
+
|
|
49
|
+
Most RAG failures are context failures, and they come in three kinds:
|
|
50
|
+
|
|
51
|
+
- **Missing.** The right passage never reached the prompt. Retrieval did not find it, or your code dropped it after retrieval.
|
|
52
|
+
- **Irrelevant.** The right passage was in the prompt, but other chunks crowded it out. The model answers correctly once they are removed.
|
|
53
|
+
- **Ignored.** The right passage was in the prompt, and the model skipped over it. The model answers correctly once that passage moves to the top.
|
|
54
|
+
|
|
55
|
+
Each label points at a different fix. Missing calls for better retrieval, such as a larger `top_k`. Irrelevant calls for a reranker or fewer chunks. Ignored calls for a different chunk order or shorter chunks. contexteval then lets you test those fixes against your test set with one command.
|
|
56
|
+
|
|
57
|
+
You do not change your app. contexteval hooks chromadb, pinecone, OpenAI, and Anthropic from the outside. Custom retrieval needs one decorator.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
contexteval needs Python 3.10 or later. The core package has no dependencies.
|
|
62
|
+
|
|
63
|
+
Once contexteval is published on PyPI, install it with the demo extras:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "contexteval[demo]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Until then, install from source:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/aakarshan-coding/context-evaluation-sdk.git
|
|
73
|
+
cd context-evaluation-sdk
|
|
74
|
+
pip install ".[demo]"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The extras are `openai`, `anthropic`, `chroma`, `pinecone`, and `demo` (chromadb plus openai). Install the ones your app uses, for example `pip install "contexteval[openai,chroma]"`, or `pip install ".[openai,chroma]"` from source.
|
|
78
|
+
|
|
79
|
+
Release steps for maintainers are in [docs/RELEASING.md](https://github.com/aakarshan-coding/context-evaluation-sdk/blob/main/docs/RELEASING.md).
|
|
80
|
+
|
|
81
|
+
## Quickstart
|
|
82
|
+
|
|
83
|
+
The bundled demo is a small RAG app over a customer-support corpus, built on chromadb and the OpenAI SDK. It has planted failures of every label. With `--provider fake` and the default demo settings, nothing calls a real API and nothing costs money.
|
|
84
|
+
|
|
85
|
+
Create a project in an empty directory:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
mkdir contexteval-demo
|
|
89
|
+
cd contexteval-demo
|
|
90
|
+
contexteval init --provider fake
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Write the planted test cases to `planted.jsonl` and import them:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m contexteval.demo.planted
|
|
97
|
+
contexteval build-tests --import planted.jsonl
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run every case through the demo app's `answer()` function:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
contexteval run contexteval.demo.app:answer --yes
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You see `run 1: 8 cases, 2 passed, 6 failed, 0 crashed`.
|
|
107
|
+
|
|
108
|
+
Label each failure:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
contexteval diagnose --yes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each failure gets a label and a line of evidence:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
app: up to 54 replay calls to contexteval.demo.app:answer (6 failures with evidence, 3 steps x 3 samples each, worst case)
|
|
118
|
+
run 1:
|
|
119
|
+
not_context When is phone support open?
|
|
120
|
+
keeping only gold (0/2) did not fix it; moving gold to the top was skipped because gold was already at the top
|
|
121
|
+
flaky Where is Trailhead Gear headquartered?
|
|
122
|
+
the unchanged baseline passed on rerun (2/2)
|
|
123
|
+
irrelevant Can fuel canisters be shipped by air?
|
|
124
|
+
keeping only the gold chunks fixed it (2/2); removed chunks 2, 3, 4 of retrieval 1; moving gold to the top was skipped because gold was already at the top
|
|
125
|
+
ignored How long do warranty repairs take?
|
|
126
|
+
moving the gold chunk to the top fixed it (2/2); gold was at chunk 3 of retrieval 1
|
|
127
|
+
missing How long is the warranty on a sleeping bag?
|
|
128
|
+
the gold chunk was never retrieved; injecting the evidence fixed it (2/2)
|
|
129
|
+
missing What is store credit for a returned tent worth?
|
|
130
|
+
the gold chunk was retrieved but dropped before the prompt; injecting the evidence fixed it (2/2)
|
|
131
|
+
diagnosed 6, skipped 0 already labeled, skipped 0 crashed, skipped 0 with no answer text
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The count after each step is samples that passed over samples drawn. The demo's fake model is deterministic, so every step settles at 2 of 2. A real model at a non-zero temperature shows splits like `(2/3)`.
|
|
135
|
+
|
|
136
|
+
Test a fix. The "never retrieved" case ranks 5th, and the demo retrieves 4 chunks:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
contexteval experiment top_k=10 --yes
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The table compares the baseline with the new setting:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
setting accuracy passed mean input tokens mean latency ms total cost
|
|
146
|
+
baseline 38% 3/8 124 62.0 unknown
|
|
147
|
+
top_k=10 50% 4/8 272 7.9 unknown
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The "never retrieved" case passes under `top_k=10`, so accuracy rises by one case. The flaky case failed only on its first call, so it passes in both rows. The cost column reads `unknown` because the fake model has no price.
|
|
151
|
+
|
|
152
|
+
Open the dashboard:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
contexteval ui
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Run the demo on the real OpenAI API
|
|
159
|
+
|
|
160
|
+
The same workflow runs against the real API. Set `OPENAI_API_KEY`, then set `CONTEXTEVAL_DEMO_LLM=openai` so the demo app calls OpenAI too. This example generates test cases from the demo corpus instead of importing the planted ones:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
mkdir contexteval-real
|
|
164
|
+
cd contexteval-real
|
|
165
|
+
export CONTEXTEVAL_DEMO_LLM=openai
|
|
166
|
+
contexteval init --provider openai
|
|
167
|
+
contexteval build-tests "$(python -c "from contexteval.demo.app import CORPUS_DIR; print(CORPUS_DIR)")" --yes
|
|
168
|
+
contexteval run contexteval.demo.app:answer --yes
|
|
169
|
+
contexteval diagnose --yes
|
|
170
|
+
contexteval experiment top_k=10 --yes
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The whole workflow made 39 calls to `gpt-4.1-mini` and cost $0.0038 in our run. A real model fails fewer cases than the planted script, so expect fewer labels.
|
|
174
|
+
|
|
175
|
+
## Point it at your own app
|
|
176
|
+
|
|
177
|
+
contexteval needs one function that takes a question and returns an answer string. You pass it to the CLI as `module:function`. The CLI imports the module from the current directory.
|
|
178
|
+
|
|
179
|
+
If your app queries chromadb or pinecone and calls OpenAI or Anthropic, it works as is. contexteval patches those clients before it imports your module.
|
|
180
|
+
|
|
181
|
+
If your app retrieves some other way (pgvector, SQL, a search API, hand-written code), mark the search function with `@contexteval.retriever`. Name the parameter that controls how many results come back, so experiments can change it. Each result item is a string or a dict whose text sits under `text`:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# my_app.py
|
|
185
|
+
import contexteval
|
|
186
|
+
import openai
|
|
187
|
+
|
|
188
|
+
DOCS = [
|
|
189
|
+
"Refunds are issued within 30 days of delivery.",
|
|
190
|
+
"Standard shipping takes four to six business days.",
|
|
191
|
+
"Phone support is open from 7 am to 9 pm Mountain Time.",
|
|
192
|
+
]
|
|
193
|
+
llm = openai.OpenAI()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@contexteval.retriever(top_k="limit")
|
|
197
|
+
def search(query: str, limit: int = 2) -> list[dict]:
|
|
198
|
+
words = set(query.lower().split())
|
|
199
|
+
ranked = sorted(DOCS, key=lambda doc: -len(words & set(doc.lower().split())))
|
|
200
|
+
return [{"id": str(i), "text": doc} for i, doc in enumerate(ranked[:limit])]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def answer(question: str) -> str:
|
|
204
|
+
context = "\n\n".join(hit["text"] for hit in search(question))
|
|
205
|
+
reply = llm.chat.completions.create(
|
|
206
|
+
model="gpt-4.1-mini",
|
|
207
|
+
messages=[{"role": "user", "content": f"Answer from this context only.\n\n{context}\n\nQuestion: {question}"}],
|
|
208
|
+
)
|
|
209
|
+
return reply.choices[0].message.content
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use `text=` to name a different key or attribute, or pass a function that returns the text. Use `top_k=None` if the result count is fixed.
|
|
213
|
+
|
|
214
|
+
Generate tests from your documents, or import your own:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
contexteval init --provider openai
|
|
218
|
+
contexteval build-tests ./docs
|
|
219
|
+
contexteval run my_app:answer
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
An import file is JSONL or CSV with `question`, `expected_answer`, and an optional `evidence` passage copied from your documents. Cases with evidence get the full diagnosis. Cases without it go to an LLM judge.
|
|
223
|
+
|
|
224
|
+
To capture production traffic, call `contexteval.init()` once at app startup. It hooks the same libraries and stores each answered question as an ungraded trace in a production run. Find the run id in `contexteval ui` and label it with `contexteval diagnose --run ID`. Under `contexteval run`, `init()` does nothing, so you can leave it in.
|
|
225
|
+
|
|
226
|
+
## Commands
|
|
227
|
+
|
|
228
|
+
Every command except `init` finds the project by walking up from the current directory to a `.contexteval/` directory. Commands that call an LLM print a cost estimate and ask before they start. `--yes` skips the question.
|
|
229
|
+
|
|
230
|
+
| Command | What it does |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `contexteval init [--provider openai\|anthropic\|fake] [--model M] [--app module:function]` | Creates `.contexteval/` with `config.json` and the database. The provider is the LLM that contexteval itself uses to write tests, grade, and judge. |
|
|
233
|
+
| `contexteval build-tests DOCS_DIR [--per-passage N] [--yes]` | Reads every `.md` and `.txt` file, asks the LLM for question, answer, and evidence, and keeps only cases whose evidence appears word for word in the document. |
|
|
234
|
+
| `contexteval build-tests --import FILE` | Imports your own JSONL or CSV cases. Makes no LLM calls. |
|
|
235
|
+
| `contexteval run [module:function] [--test-set NAME] [--yes]` | Runs every case through your function and grades each answer. Remembers the app for later commands. |
|
|
236
|
+
| `contexteval diagnose [--run ID] [--samples N] [--force] [--yes]` | Labels each failure of the latest baseline run, or of run `ID`. Each replay step draws up to `N` samples (default 3, odd) and a majority decides. Skips failures that already have a label unless you pass `--force`. |
|
|
237
|
+
| `contexteval experiment [module:function] KNOB=v1,v2 ... [--yes]` | Runs every case under each combination of settings and prints a comparison table. |
|
|
238
|
+
| `contexteval ui [--port 8765] [--no-open]` | Serves the dashboard at `http://127.0.0.1:8765/`. |
|
|
239
|
+
|
|
240
|
+
Grading checks first whether the expected answer appears in the answer, after normalizing case and punctuation. If it does not, the LLM decides whether the two agree, at temperature 0.
|
|
241
|
+
|
|
242
|
+
## How diagnosis picks a label
|
|
243
|
+
|
|
244
|
+
contexteval works through these steps for each failed case and stops at the first one that decides:
|
|
245
|
+
|
|
246
|
+
1. If the case has no evidence passage, an LLM judge reads the trace and picks a label. Production traces always take this path. The judge can also say there was no failure.
|
|
247
|
+
2. contexteval reruns the case unchanged. If it passes now, the label is `flaky`.
|
|
248
|
+
3. contexteval looks for the evidence in what the app sent. If it is not in the prompt, the label is `missing`. The cause is `not_retrieved` when no retrieval returned it and `dropped` when retrieval returned it but your code left it out of the prompt. One more run puts the evidence first in the context and records whether that fixes the answer.
|
|
249
|
+
4. If the evidence was in the prompt, contexteval reruns with that chunk moved to the top and every other chunk kept. If the answer is now right, the label is `ignored`.
|
|
250
|
+
5. Otherwise it reruns with only the chunks that contain the evidence. If the answer is now right, the label is `irrelevant`.
|
|
251
|
+
6. Otherwise the label is `not_context`. The context held the answer and the model still got it wrong. Look at the prompt, the model, or the expected answer.
|
|
252
|
+
|
|
253
|
+
The order matters. Moving one chunk is a smaller change than removing chunks, so `ignored` wins when both would work. Replays never change your app's LLM settings.
|
|
254
|
+
|
|
255
|
+
Each step is sampled, because an app that calls its model at a non-zero temperature can pass a replay by luck. A step draws up to `--samples N` replays (default 3; `N` must be odd) and stops as soon as a majority of `N` agree, so a step that passes twice in a row costs 2 calls, and a split step costs 3. A step passes when a majority of its samples pass. `flaky` means a majority of the unchanged reruns passed. Every label line shows the count for the step that decided it, as in `(2/3)`, and the dashboard shows every sample's answer under its step. A case costs between 2 and 3 times `N` runs of your app; the cost prompt states the worst case. `--samples 1` decides from one replay per step, as versions before 0.2 did. Labels stored by those versions still load and show as `(1/1)`.
|
|
256
|
+
|
|
257
|
+
Chunk and retrieval positions in label lines and in the dashboard count from 1.
|
|
258
|
+
|
|
259
|
+
A passage counts as present when the chunk or prompt contains the evidence text after normalization. It also counts when 80% of the evidence words appear inside a window about 1.5 times the evidence length. The second rule catches evidence that your chunker split in two.
|
|
260
|
+
|
|
261
|
+
## Experiment knobs
|
|
262
|
+
|
|
263
|
+
Each knob takes a comma-separated list of values. contexteval runs every combination, always with a baseline row first.
|
|
264
|
+
|
|
265
|
+
| Knob | Values | Effect |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `top_k` | a positive integer | Overrides the result count passed to the retrieval call. |
|
|
268
|
+
| `rerank` | `none`, `lexical` | `lexical` reorders chunks by word overlap with the question. No model download. |
|
|
269
|
+
| `order` | `as_is`, `reverse`, `ends` | `ends` puts the best chunks at the start and end of the context, for models that lose the middle. |
|
|
270
|
+
| `max_chunks` | a positive integer | Keeps only the first N chunks. |
|
|
271
|
+
| `compress` | a positive integer | Keeps the N sentences of each chunk with the most word overlap with the question. |
|
|
272
|
+
|
|
273
|
+
For example, `contexteval experiment top_k=3,10 rerank=none,lexical` runs five settings: the baseline and four combinations. The table shows accuracy, cases passed, mean input tokens, mean latency, and total app cost per setting. Experiments never see the evidence passages, so a setting cannot win by peeking at the answer.
|
|
274
|
+
|
|
275
|
+
## Dashboard
|
|
276
|
+
|
|
277
|
+
`contexteval ui` serves a local page with every run, each failure with its label and evidence line, the retrieved and delivered chunks, and the experiment tables. It binds to `127.0.0.1` only and rejects requests with any other `Host` header.
|
|
278
|
+
|
|
279
|
+
## Cost and privacy
|
|
280
|
+
|
|
281
|
+
Everything stays on your machine. contexteval stores test cases, traces (prompts, answers, and chunks), and labels in `.contexteval/contexteval.db`, a local SQLite file. There is no hosted service and no telemetry.
|
|
282
|
+
|
|
283
|
+
LLM calls go to your provider with your own key, read from `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. The default models are `gpt-4.1-mini` for OpenAI and `claude-haiku-4-5` for Anthropic. Change them with `contexteval init --model`. contexteval's own calls are never recorded as traces. They run at temperature 0 on every model that accepts it (see Limits).
|
|
284
|
+
|
|
285
|
+
The estimate counts characters divided by 4 as tokens, priced from a built-in table. It assumes every answer needs an LLM grade, so the real cost is usually lower. contexteval does not report actual spend after a command. Your provider's usage page has the exact figure.
|
|
286
|
+
|
|
287
|
+
## Limits in v1
|
|
288
|
+
|
|
289
|
+
- Hooked calls are chromadb `Collection.query` and `AsyncCollection.query`, pinecone `Index.query`, OpenAI chat completions and responses (`OpenAI` and `AsyncOpenAI`), and Anthropic messages (`Anthropic` and `AsyncAnthropic`). The pinecone hook is tested against mocks only, and pinecone's async index is not hooked.
|
|
290
|
+
- `@contexteval.retriever` accepts `async def` functions, and `answer` may be `async def`. The CLI runs an async `answer` on one event loop for the whole command, so clients bound to the loop stay valid across questions. In production each asyncio task has its own capture buffer.
|
|
291
|
+
- pgvector has no client library to hook. Use `@contexteval.retriever` on your query function.
|
|
292
|
+
- Production traces are split by a heuristic. Each LLM call closes one trace that holds the retrievals since the previous LLM call. An agent that calls the LLM several times per question produces several traces.
|
|
293
|
+
- Cases run one at a time, in the same process as the CLI.
|
|
294
|
+
- An experiment setting applies to every retrieval call in a run.
|
|
295
|
+
- A chromadb call with several query texts is recorded but never changed by an experiment.
|
|
296
|
+
- Replay runs are not stored as traces. Their outcomes are stored inside the label.
|
|
297
|
+
- Current Claude models such as `claude-haiku-5-5` and `claude-sonnet-5-5` reject non-default sampling parameters. On those models contexteval omits `temperature`, so grading and judging do not run at temperature 0 and can vary between runs. The Anthropic default stays `claude-haiku-4-5` for that reason.
|
|
298
|
+
|
|
299
|
+
## Roadmap
|
|
300
|
+
|
|
301
|
+
- v2. An automatic sweep that picks experiments from the labels: larger `top_k` for missing, reranking for irrelevant, reordering for ignored. LangChain and LlamaIndex plugins.
|
|
302
|
+
- v3. A background optimizer that replays sampled production traces and suggests changes. OpenTelemetry import.
|
|
303
|
+
- v4. Support for long-running tool agents. A hosted team version.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# contexteval
|
|
2
|
+
|
|
3
|
+
contexteval tells you why your RAG app gave a wrong answer. It runs your app on test questions, records what was retrieved and what reached the prompt, and labels each failure.
|
|
4
|
+
|
|
5
|
+
Most RAG failures are context failures, and they come in three kinds:
|
|
6
|
+
|
|
7
|
+
- **Missing.** The right passage never reached the prompt. Retrieval did not find it, or your code dropped it after retrieval.
|
|
8
|
+
- **Irrelevant.** The right passage was in the prompt, but other chunks crowded it out. The model answers correctly once they are removed.
|
|
9
|
+
- **Ignored.** The right passage was in the prompt, and the model skipped over it. The model answers correctly once that passage moves to the top.
|
|
10
|
+
|
|
11
|
+
Each label points at a different fix. Missing calls for better retrieval, such as a larger `top_k`. Irrelevant calls for a reranker or fewer chunks. Ignored calls for a different chunk order or shorter chunks. contexteval then lets you test those fixes against your test set with one command.
|
|
12
|
+
|
|
13
|
+
You do not change your app. contexteval hooks chromadb, pinecone, OpenAI, and Anthropic from the outside. Custom retrieval needs one decorator.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
contexteval needs Python 3.10 or later. The core package has no dependencies.
|
|
18
|
+
|
|
19
|
+
Once contexteval is published on PyPI, install it with the demo extras:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install "contexteval[demo]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Until then, install from source:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/aakarshan-coding/context-evaluation-sdk.git
|
|
29
|
+
cd context-evaluation-sdk
|
|
30
|
+
pip install ".[demo]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The extras are `openai`, `anthropic`, `chroma`, `pinecone`, and `demo` (chromadb plus openai). Install the ones your app uses, for example `pip install "contexteval[openai,chroma]"`, or `pip install ".[openai,chroma]"` from source.
|
|
34
|
+
|
|
35
|
+
Release steps for maintainers are in [docs/RELEASING.md](https://github.com/aakarshan-coding/context-evaluation-sdk/blob/main/docs/RELEASING.md).
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
The bundled demo is a small RAG app over a customer-support corpus, built on chromadb and the OpenAI SDK. It has planted failures of every label. With `--provider fake` and the default demo settings, nothing calls a real API and nothing costs money.
|
|
40
|
+
|
|
41
|
+
Create a project in an empty directory:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
mkdir contexteval-demo
|
|
45
|
+
cd contexteval-demo
|
|
46
|
+
contexteval init --provider fake
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Write the planted test cases to `planted.jsonl` and import them:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m contexteval.demo.planted
|
|
53
|
+
contexteval build-tests --import planted.jsonl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run every case through the demo app's `answer()` function:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
contexteval run contexteval.demo.app:answer --yes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You see `run 1: 8 cases, 2 passed, 6 failed, 0 crashed`.
|
|
63
|
+
|
|
64
|
+
Label each failure:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
contexteval diagnose --yes
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Each failure gets a label and a line of evidence:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
app: up to 54 replay calls to contexteval.demo.app:answer (6 failures with evidence, 3 steps x 3 samples each, worst case)
|
|
74
|
+
run 1:
|
|
75
|
+
not_context When is phone support open?
|
|
76
|
+
keeping only gold (0/2) did not fix it; moving gold to the top was skipped because gold was already at the top
|
|
77
|
+
flaky Where is Trailhead Gear headquartered?
|
|
78
|
+
the unchanged baseline passed on rerun (2/2)
|
|
79
|
+
irrelevant Can fuel canisters be shipped by air?
|
|
80
|
+
keeping only the gold chunks fixed it (2/2); removed chunks 2, 3, 4 of retrieval 1; moving gold to the top was skipped because gold was already at the top
|
|
81
|
+
ignored How long do warranty repairs take?
|
|
82
|
+
moving the gold chunk to the top fixed it (2/2); gold was at chunk 3 of retrieval 1
|
|
83
|
+
missing How long is the warranty on a sleeping bag?
|
|
84
|
+
the gold chunk was never retrieved; injecting the evidence fixed it (2/2)
|
|
85
|
+
missing What is store credit for a returned tent worth?
|
|
86
|
+
the gold chunk was retrieved but dropped before the prompt; injecting the evidence fixed it (2/2)
|
|
87
|
+
diagnosed 6, skipped 0 already labeled, skipped 0 crashed, skipped 0 with no answer text
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The count after each step is samples that passed over samples drawn. The demo's fake model is deterministic, so every step settles at 2 of 2. A real model at a non-zero temperature shows splits like `(2/3)`.
|
|
91
|
+
|
|
92
|
+
Test a fix. The "never retrieved" case ranks 5th, and the demo retrieves 4 chunks:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
contexteval experiment top_k=10 --yes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The table compares the baseline with the new setting:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
setting accuracy passed mean input tokens mean latency ms total cost
|
|
102
|
+
baseline 38% 3/8 124 62.0 unknown
|
|
103
|
+
top_k=10 50% 4/8 272 7.9 unknown
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The "never retrieved" case passes under `top_k=10`, so accuracy rises by one case. The flaky case failed only on its first call, so it passes in both rows. The cost column reads `unknown` because the fake model has no price.
|
|
107
|
+
|
|
108
|
+
Open the dashboard:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
contexteval ui
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Run the demo on the real OpenAI API
|
|
115
|
+
|
|
116
|
+
The same workflow runs against the real API. Set `OPENAI_API_KEY`, then set `CONTEXTEVAL_DEMO_LLM=openai` so the demo app calls OpenAI too. This example generates test cases from the demo corpus instead of importing the planted ones:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
mkdir contexteval-real
|
|
120
|
+
cd contexteval-real
|
|
121
|
+
export CONTEXTEVAL_DEMO_LLM=openai
|
|
122
|
+
contexteval init --provider openai
|
|
123
|
+
contexteval build-tests "$(python -c "from contexteval.demo.app import CORPUS_DIR; print(CORPUS_DIR)")" --yes
|
|
124
|
+
contexteval run contexteval.demo.app:answer --yes
|
|
125
|
+
contexteval diagnose --yes
|
|
126
|
+
contexteval experiment top_k=10 --yes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The whole workflow made 39 calls to `gpt-4.1-mini` and cost $0.0038 in our run. A real model fails fewer cases than the planted script, so expect fewer labels.
|
|
130
|
+
|
|
131
|
+
## Point it at your own app
|
|
132
|
+
|
|
133
|
+
contexteval needs one function that takes a question and returns an answer string. You pass it to the CLI as `module:function`. The CLI imports the module from the current directory.
|
|
134
|
+
|
|
135
|
+
If your app queries chromadb or pinecone and calls OpenAI or Anthropic, it works as is. contexteval patches those clients before it imports your module.
|
|
136
|
+
|
|
137
|
+
If your app retrieves some other way (pgvector, SQL, a search API, hand-written code), mark the search function with `@contexteval.retriever`. Name the parameter that controls how many results come back, so experiments can change it. Each result item is a string or a dict whose text sits under `text`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# my_app.py
|
|
141
|
+
import contexteval
|
|
142
|
+
import openai
|
|
143
|
+
|
|
144
|
+
DOCS = [
|
|
145
|
+
"Refunds are issued within 30 days of delivery.",
|
|
146
|
+
"Standard shipping takes four to six business days.",
|
|
147
|
+
"Phone support is open from 7 am to 9 pm Mountain Time.",
|
|
148
|
+
]
|
|
149
|
+
llm = openai.OpenAI()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@contexteval.retriever(top_k="limit")
|
|
153
|
+
def search(query: str, limit: int = 2) -> list[dict]:
|
|
154
|
+
words = set(query.lower().split())
|
|
155
|
+
ranked = sorted(DOCS, key=lambda doc: -len(words & set(doc.lower().split())))
|
|
156
|
+
return [{"id": str(i), "text": doc} for i, doc in enumerate(ranked[:limit])]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def answer(question: str) -> str:
|
|
160
|
+
context = "\n\n".join(hit["text"] for hit in search(question))
|
|
161
|
+
reply = llm.chat.completions.create(
|
|
162
|
+
model="gpt-4.1-mini",
|
|
163
|
+
messages=[{"role": "user", "content": f"Answer from this context only.\n\n{context}\n\nQuestion: {question}"}],
|
|
164
|
+
)
|
|
165
|
+
return reply.choices[0].message.content
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use `text=` to name a different key or attribute, or pass a function that returns the text. Use `top_k=None` if the result count is fixed.
|
|
169
|
+
|
|
170
|
+
Generate tests from your documents, or import your own:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
contexteval init --provider openai
|
|
174
|
+
contexteval build-tests ./docs
|
|
175
|
+
contexteval run my_app:answer
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
An import file is JSONL or CSV with `question`, `expected_answer`, and an optional `evidence` passage copied from your documents. Cases with evidence get the full diagnosis. Cases without it go to an LLM judge.
|
|
179
|
+
|
|
180
|
+
To capture production traffic, call `contexteval.init()` once at app startup. It hooks the same libraries and stores each answered question as an ungraded trace in a production run. Find the run id in `contexteval ui` and label it with `contexteval diagnose --run ID`. Under `contexteval run`, `init()` does nothing, so you can leave it in.
|
|
181
|
+
|
|
182
|
+
## Commands
|
|
183
|
+
|
|
184
|
+
Every command except `init` finds the project by walking up from the current directory to a `.contexteval/` directory. Commands that call an LLM print a cost estimate and ask before they start. `--yes` skips the question.
|
|
185
|
+
|
|
186
|
+
| Command | What it does |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `contexteval init [--provider openai\|anthropic\|fake] [--model M] [--app module:function]` | Creates `.contexteval/` with `config.json` and the database. The provider is the LLM that contexteval itself uses to write tests, grade, and judge. |
|
|
189
|
+
| `contexteval build-tests DOCS_DIR [--per-passage N] [--yes]` | Reads every `.md` and `.txt` file, asks the LLM for question, answer, and evidence, and keeps only cases whose evidence appears word for word in the document. |
|
|
190
|
+
| `contexteval build-tests --import FILE` | Imports your own JSONL or CSV cases. Makes no LLM calls. |
|
|
191
|
+
| `contexteval run [module:function] [--test-set NAME] [--yes]` | Runs every case through your function and grades each answer. Remembers the app for later commands. |
|
|
192
|
+
| `contexteval diagnose [--run ID] [--samples N] [--force] [--yes]` | Labels each failure of the latest baseline run, or of run `ID`. Each replay step draws up to `N` samples (default 3, odd) and a majority decides. Skips failures that already have a label unless you pass `--force`. |
|
|
193
|
+
| `contexteval experiment [module:function] KNOB=v1,v2 ... [--yes]` | Runs every case under each combination of settings and prints a comparison table. |
|
|
194
|
+
| `contexteval ui [--port 8765] [--no-open]` | Serves the dashboard at `http://127.0.0.1:8765/`. |
|
|
195
|
+
|
|
196
|
+
Grading checks first whether the expected answer appears in the answer, after normalizing case and punctuation. If it does not, the LLM decides whether the two agree, at temperature 0.
|
|
197
|
+
|
|
198
|
+
## How diagnosis picks a label
|
|
199
|
+
|
|
200
|
+
contexteval works through these steps for each failed case and stops at the first one that decides:
|
|
201
|
+
|
|
202
|
+
1. If the case has no evidence passage, an LLM judge reads the trace and picks a label. Production traces always take this path. The judge can also say there was no failure.
|
|
203
|
+
2. contexteval reruns the case unchanged. If it passes now, the label is `flaky`.
|
|
204
|
+
3. contexteval looks for the evidence in what the app sent. If it is not in the prompt, the label is `missing`. The cause is `not_retrieved` when no retrieval returned it and `dropped` when retrieval returned it but your code left it out of the prompt. One more run puts the evidence first in the context and records whether that fixes the answer.
|
|
205
|
+
4. If the evidence was in the prompt, contexteval reruns with that chunk moved to the top and every other chunk kept. If the answer is now right, the label is `ignored`.
|
|
206
|
+
5. Otherwise it reruns with only the chunks that contain the evidence. If the answer is now right, the label is `irrelevant`.
|
|
207
|
+
6. Otherwise the label is `not_context`. The context held the answer and the model still got it wrong. Look at the prompt, the model, or the expected answer.
|
|
208
|
+
|
|
209
|
+
The order matters. Moving one chunk is a smaller change than removing chunks, so `ignored` wins when both would work. Replays never change your app's LLM settings.
|
|
210
|
+
|
|
211
|
+
Each step is sampled, because an app that calls its model at a non-zero temperature can pass a replay by luck. A step draws up to `--samples N` replays (default 3; `N` must be odd) and stops as soon as a majority of `N` agree, so a step that passes twice in a row costs 2 calls, and a split step costs 3. A step passes when a majority of its samples pass. `flaky` means a majority of the unchanged reruns passed. Every label line shows the count for the step that decided it, as in `(2/3)`, and the dashboard shows every sample's answer under its step. A case costs between 2 and 3 times `N` runs of your app; the cost prompt states the worst case. `--samples 1` decides from one replay per step, as versions before 0.2 did. Labels stored by those versions still load and show as `(1/1)`.
|
|
212
|
+
|
|
213
|
+
Chunk and retrieval positions in label lines and in the dashboard count from 1.
|
|
214
|
+
|
|
215
|
+
A passage counts as present when the chunk or prompt contains the evidence text after normalization. It also counts when 80% of the evidence words appear inside a window about 1.5 times the evidence length. The second rule catches evidence that your chunker split in two.
|
|
216
|
+
|
|
217
|
+
## Experiment knobs
|
|
218
|
+
|
|
219
|
+
Each knob takes a comma-separated list of values. contexteval runs every combination, always with a baseline row first.
|
|
220
|
+
|
|
221
|
+
| Knob | Values | Effect |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `top_k` | a positive integer | Overrides the result count passed to the retrieval call. |
|
|
224
|
+
| `rerank` | `none`, `lexical` | `lexical` reorders chunks by word overlap with the question. No model download. |
|
|
225
|
+
| `order` | `as_is`, `reverse`, `ends` | `ends` puts the best chunks at the start and end of the context, for models that lose the middle. |
|
|
226
|
+
| `max_chunks` | a positive integer | Keeps only the first N chunks. |
|
|
227
|
+
| `compress` | a positive integer | Keeps the N sentences of each chunk with the most word overlap with the question. |
|
|
228
|
+
|
|
229
|
+
For example, `contexteval experiment top_k=3,10 rerank=none,lexical` runs five settings: the baseline and four combinations. The table shows accuracy, cases passed, mean input tokens, mean latency, and total app cost per setting. Experiments never see the evidence passages, so a setting cannot win by peeking at the answer.
|
|
230
|
+
|
|
231
|
+
## Dashboard
|
|
232
|
+
|
|
233
|
+
`contexteval ui` serves a local page with every run, each failure with its label and evidence line, the retrieved and delivered chunks, and the experiment tables. It binds to `127.0.0.1` only and rejects requests with any other `Host` header.
|
|
234
|
+
|
|
235
|
+
## Cost and privacy
|
|
236
|
+
|
|
237
|
+
Everything stays on your machine. contexteval stores test cases, traces (prompts, answers, and chunks), and labels in `.contexteval/contexteval.db`, a local SQLite file. There is no hosted service and no telemetry.
|
|
238
|
+
|
|
239
|
+
LLM calls go to your provider with your own key, read from `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. The default models are `gpt-4.1-mini` for OpenAI and `claude-haiku-4-5` for Anthropic. Change them with `contexteval init --model`. contexteval's own calls are never recorded as traces. They run at temperature 0 on every model that accepts it (see Limits).
|
|
240
|
+
|
|
241
|
+
The estimate counts characters divided by 4 as tokens, priced from a built-in table. It assumes every answer needs an LLM grade, so the real cost is usually lower. contexteval does not report actual spend after a command. Your provider's usage page has the exact figure.
|
|
242
|
+
|
|
243
|
+
## Limits in v1
|
|
244
|
+
|
|
245
|
+
- Hooked calls are chromadb `Collection.query` and `AsyncCollection.query`, pinecone `Index.query`, OpenAI chat completions and responses (`OpenAI` and `AsyncOpenAI`), and Anthropic messages (`Anthropic` and `AsyncAnthropic`). The pinecone hook is tested against mocks only, and pinecone's async index is not hooked.
|
|
246
|
+
- `@contexteval.retriever` accepts `async def` functions, and `answer` may be `async def`. The CLI runs an async `answer` on one event loop for the whole command, so clients bound to the loop stay valid across questions. In production each asyncio task has its own capture buffer.
|
|
247
|
+
- pgvector has no client library to hook. Use `@contexteval.retriever` on your query function.
|
|
248
|
+
- Production traces are split by a heuristic. Each LLM call closes one trace that holds the retrievals since the previous LLM call. An agent that calls the LLM several times per question produces several traces.
|
|
249
|
+
- Cases run one at a time, in the same process as the CLI.
|
|
250
|
+
- An experiment setting applies to every retrieval call in a run.
|
|
251
|
+
- A chromadb call with several query texts is recorded but never changed by an experiment.
|
|
252
|
+
- Replay runs are not stored as traces. Their outcomes are stored inside the label.
|
|
253
|
+
- Current Claude models such as `claude-haiku-5-5` and `claude-sonnet-5-5` reject non-default sampling parameters. On those models contexteval omits `temperature`, so grading and judging do not run at temperature 0 and can vary between runs. The Anthropic default stays `claude-haiku-4-5` for that reason.
|
|
254
|
+
|
|
255
|
+
## Roadmap
|
|
256
|
+
|
|
257
|
+
- v2. An automatic sweep that picks experiments from the labels: larger `top_k` for missing, reranking for irrelevant, reordering for ignored. LangChain and LlamaIndex plugins.
|
|
258
|
+
- v3. A background optimizer that replays sampled production traces and suggests changes. OpenTelemetry import.
|
|
259
|
+
- v4. Support for long-running tool agents. A hosted team version.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""contexteval: find out why your RAG agent's context failed.
|
|
2
|
+
|
|
3
|
+
Public surface is deliberately two names for app code plus read-only types:
|
|
4
|
+
|
|
5
|
+
import contexteval
|
|
6
|
+
contexteval.init() # hook chromadb, pinecone, openai, anthropic
|
|
7
|
+
|
|
8
|
+
@contexteval.retriever(top_k="k") # custom retrieval (pgvector, SQL, APIs)
|
|
9
|
+
def search(query: str, k: int = 5) -> list[dict]: ...
|
|
10
|
+
|
|
11
|
+
Everything else is driven by the `contexteval` CLI. Modules other than this one and
|
|
12
|
+
`contexteval.types` are internal; their names may change without notice.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._capture import InstallReport, retriever
|
|
16
|
+
from ._hooks import init
|
|
17
|
+
from ._intervention import InterventionNotApplicable
|
|
18
|
+
|
|
19
|
+
__all__ = ["init", "retriever", "InstallReport", "InterventionNotApplicable"]
|