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.
Files changed (54) hide show
  1. contexteval-0.2.0/CHANGELOG.md +36 -0
  2. contexteval-0.2.0/LICENSE +21 -0
  3. contexteval-0.2.0/MANIFEST.in +2 -0
  4. contexteval-0.2.0/PKG-INFO +307 -0
  5. contexteval-0.2.0/README.md +263 -0
  6. contexteval-0.2.0/contexteval/__init__.py +19 -0
  7. contexteval-0.2.0/contexteval/__main__.py +7 -0
  8. contexteval-0.2.0/contexteval/_capture.py +821 -0
  9. contexteval-0.2.0/contexteval/_cli.py +477 -0
  10. contexteval-0.2.0/contexteval/_diagnose.py +347 -0
  11. contexteval-0.2.0/contexteval/_hooks.py +439 -0
  12. contexteval-0.2.0/contexteval/_intervention.py +203 -0
  13. contexteval-0.2.0/contexteval/_llm.py +248 -0
  14. contexteval-0.2.0/contexteval/_runner.py +196 -0
  15. contexteval-0.2.0/contexteval/_store.py +489 -0
  16. contexteval-0.2.0/contexteval/_testset.py +191 -0
  17. contexteval-0.2.0/contexteval/_text.py +124 -0
  18. contexteval-0.2.0/contexteval/_ui.py +309 -0
  19. contexteval-0.2.0/contexteval/demo/__init__.py +1 -0
  20. contexteval-0.2.0/contexteval/demo/_lock.py +39 -0
  21. contexteval-0.2.0/contexteval/demo/app.py +169 -0
  22. contexteval-0.2.0/contexteval/demo/corpus/company.md +11 -0
  23. contexteval-0.2.0/contexteval/demo/corpus/returns.md +11 -0
  24. contexteval-0.2.0/contexteval/demo/corpus/shipping.md +13 -0
  25. contexteval-0.2.0/contexteval/demo/corpus/warranty.md +15 -0
  26. contexteval-0.2.0/contexteval/demo/fake_llm.py +161 -0
  27. contexteval-0.2.0/contexteval/demo/planted.py +132 -0
  28. contexteval-0.2.0/contexteval/types.py +364 -0
  29. contexteval-0.2.0/contexteval/ui.html +433 -0
  30. contexteval-0.2.0/contexteval.egg-info/PKG-INFO +307 -0
  31. contexteval-0.2.0/contexteval.egg-info/SOURCES.txt +52 -0
  32. contexteval-0.2.0/contexteval.egg-info/dependency_links.txt +1 -0
  33. contexteval-0.2.0/contexteval.egg-info/entry_points.txt +2 -0
  34. contexteval-0.2.0/contexteval.egg-info/requires.txt +23 -0
  35. contexteval-0.2.0/contexteval.egg-info/top_level.txt +1 -0
  36. contexteval-0.2.0/pyproject.toml +56 -0
  37. contexteval-0.2.0/setup.cfg +4 -0
  38. contexteval-0.2.0/tests/conftest.py +73 -0
  39. contexteval-0.2.0/tests/test_acceptance.py +66 -0
  40. contexteval-0.2.0/tests/test_capture.py +351 -0
  41. contexteval-0.2.0/tests/test_cli.py +257 -0
  42. contexteval-0.2.0/tests/test_demo.py +197 -0
  43. contexteval-0.2.0/tests/test_diagnose.py +319 -0
  44. contexteval-0.2.0/tests/test_diagnose_table.py +231 -0
  45. contexteval-0.2.0/tests/test_experiment.py +194 -0
  46. contexteval-0.2.0/tests/test_hooks.py +696 -0
  47. contexteval-0.2.0/tests/test_intervention.py +135 -0
  48. contexteval-0.2.0/tests/test_llm.py +49 -0
  49. contexteval-0.2.0/tests/test_retriever.py +282 -0
  50. contexteval-0.2.0/tests/test_runner.py +199 -0
  51. contexteval-0.2.0/tests/test_store.py +245 -0
  52. contexteval-0.2.0/tests/test_testset.py +131 -0
  53. contexteval-0.2.0/tests/test_text.py +131 -0
  54. 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,2 @@
1
+ include CHANGELOG.md
2
+ include tests/conftest.py
@@ -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"]