evalkit-harness 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalkit_harness-0.1.0/.gitignore +26 -0
- evalkit_harness-0.1.0/LICENSE +21 -0
- evalkit_harness-0.1.0/PKG-INFO +70 -0
- evalkit_harness-0.1.0/README.md +36 -0
- evalkit_harness-0.1.0/changelog.md +74 -0
- evalkit_harness-0.1.0/pyproject.toml +94 -0
- evalkit_harness-0.1.0/src/evalkit/__init__.py +5 -0
- evalkit_harness-0.1.0/src/evalkit/adapters/__init__.py +25 -0
- evalkit_harness-0.1.0/src/evalkit/adapters/anthropic.py +179 -0
- evalkit_harness-0.1.0/src/evalkit/adapters/base.py +176 -0
- evalkit_harness-0.1.0/src/evalkit/adapters/openai.py +255 -0
- evalkit_harness-0.1.0/src/evalkit/agents/__init__.py +0 -0
- evalkit_harness-0.1.0/src/evalkit/agents/aggregate.py +414 -0
- evalkit_harness-0.1.0/src/evalkit/agents/db.py +475 -0
- evalkit_harness-0.1.0/src/evalkit/agents/loop.py +473 -0
- evalkit_harness-0.1.0/src/evalkit/agents/runner.py +352 -0
- evalkit_harness-0.1.0/src/evalkit/agents/sandbox.py +418 -0
- evalkit_harness-0.1.0/src/evalkit/agents/score.py +94 -0
- evalkit_harness-0.1.0/src/evalkit/agents/simuser.py +470 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-01.yaml +22 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-02.yaml +29 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-03.yaml +26 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-04.yaml +27 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-05.yaml +41 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-06.yaml +45 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-07.yaml +30 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-08.yaml +23 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-09.yaml +42 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-10.yaml +23 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-11.yaml +31 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-12.yaml +32 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-13.yaml +32 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-14.yaml +27 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-15.yaml +30 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-16.yaml +22 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-01.yaml +318 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-02.yaml +19 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-03.yaml +17 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-04.yaml +318 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-05.yaml +19 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-06.yaml +17 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-07.yaml +318 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-08.yaml +19 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-09.yaml +16 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-10.yaml +317 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-11.yaml +18 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-12.yaml +16 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-13.yaml +317 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-14.yaml +18 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-15.yaml +16 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-16.yaml +317 -0
- evalkit_harness-0.1.0/src/evalkit/agents/tasks.py +184 -0
- evalkit_harness-0.1.0/src/evalkit/cli.py +739 -0
- evalkit_harness-0.1.0/src/evalkit/config.py +75 -0
- evalkit_harness-0.1.0/src/evalkit/cost.py +239 -0
- evalkit_harness-0.1.0/src/evalkit/datasets/__init__.py +15 -0
- evalkit_harness-0.1.0/src/evalkit/datasets/local.py +146 -0
- evalkit_harness-0.1.0/src/evalkit/datasets/pubmedqa.py +130 -0
- evalkit_harness-0.1.0/src/evalkit/datasets/scifact.py +159 -0
- evalkit_harness-0.1.0/src/evalkit/demo/__init__.py +15 -0
- evalkit_harness-0.1.0/src/evalkit/demo/engine.py +486 -0
- evalkit_harness-0.1.0/src/evalkit/demo/guard.py +282 -0
- evalkit_harness-0.1.0/src/evalkit/demo/page.py +377 -0
- evalkit_harness-0.1.0/src/evalkit/demo/pubmed.py +66 -0
- evalkit_harness-0.1.0/src/evalkit/env.py +42 -0
- evalkit_harness-0.1.0/src/evalkit/examples/__init__.py +0 -0
- evalkit_harness-0.1.0/src/evalkit/examples/example_run/cases.jsonl +12 -0
- evalkit_harness-0.1.0/src/evalkit/examples/example_run/records.jsonl +36 -0
- evalkit_harness-0.1.0/src/evalkit/examples/example_run/run.json +35 -0
- evalkit_harness-0.1.0/src/evalkit/examples/example_run/summary.json +325 -0
- evalkit_harness-0.1.0/src/evalkit/onboarding.py +280 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/__init__.py +1 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/aggregate.py +382 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/answer.py +68 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/calibrate.py +560 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/extract.py +398 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/generate.py +233 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/judge.py +832 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/labeller.html +53 -0
- evalkit_harness-0.1.0/src/evalkit/pipeline/labeller.py +201 -0
- evalkit_harness-0.1.0/src/evalkit/prices.yaml +24 -0
- evalkit_harness-0.1.0/src/evalkit/py.typed +0 -0
- evalkit_harness-0.1.0/src/evalkit/ratelimit.py +46 -0
- evalkit_harness-0.1.0/src/evalkit/report/__init__.py +5 -0
- evalkit_harness-0.1.0/src/evalkit/report/agents.py +242 -0
- evalkit_harness-0.1.0/src/evalkit/report/html.py +502 -0
- evalkit_harness-0.1.0/src/evalkit/report/readme.py +205 -0
- evalkit_harness-0.1.0/src/evalkit/report/svg.py +254 -0
- evalkit_harness-0.1.0/src/evalkit/retry.py +62 -0
- evalkit_harness-0.1.0/src/evalkit/run.py +655 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/__init__.py +20 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/canonical.py +42 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/code_versions.json +26 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/executor.py +332 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/node.py +70 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/store.py +152 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/versions.py +164 -0
- evalkit_harness-0.1.0/src/evalkit/runtime/writer.py +63 -0
- evalkit_harness-0.1.0/src/evalkit/sampling.py +52 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/__init__.py +58 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/_base.py +101 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/agent.py +127 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/benchmark.py +128 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/calibration.py +51 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/config.py +86 -0
- evalkit_harness-0.1.0/src/evalkit/schemas/summary.py +173 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/__init__.py +30 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/attack.py +81 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/base.py +164 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/deterministic.py +52 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/faithfulness.py +180 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/retrieval.py +83 -0
- evalkit_harness-0.1.0/src/evalkit/scorers/sql.py +306 -0
- evalkit_harness-0.1.0/src/evalkit/smoke.py +137 -0
- evalkit_harness-0.1.0/src/evalkit/stats.py +117 -0
- evalkit_harness-0.1.0/src/evalkit/tasks/__init__.py +32 -0
- evalkit_harness-0.1.0/src/evalkit/tasks/pubmedqa.yaml +25 -0
- evalkit_harness-0.1.0/src/evalkit/tasks/scifact.yaml +27 -0
- evalkit_harness-0.1.0/src/evalkit/text/__init__.py +5 -0
- evalkit_harness-0.1.0/src/evalkit/text/sentences.py +138 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.harness/
|
|
10
|
+
.evalkit/
|
|
11
|
+
.env
|
|
12
|
+
.DS_Store
|
|
13
|
+
|
|
14
|
+
# personal notes, not for the public repo
|
|
15
|
+
Portfolio notes.md
|
|
16
|
+
.venv-py*/
|
|
17
|
+
runs/
|
|
18
|
+
heldout/*
|
|
19
|
+
!heldout/MANIFEST.json
|
|
20
|
+
|
|
21
|
+
# Streamlit local secrets
|
|
22
|
+
.streamlit/secrets.toml
|
|
23
|
+
|
|
24
|
+
# Claude Code local settings
|
|
25
|
+
.claude/settings.local.json
|
|
26
|
+
.claude/scheduled_tasks.lock
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Biplob Chakraborty
|
|
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,70 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evalkit-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Evaluate your own task across Claude and OpenAI models: grounded, calibrated, cost-aware, with confidence intervals.
|
|
5
|
+
Project-URL: Homepage, https://github.com/biplob819/evalkit
|
|
6
|
+
Project-URL: Issues, https://github.com/biplob819/evalkit/issues
|
|
7
|
+
Author: Biplob Chakraborty
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,benchmark,evaluation,groundedness,llm,llm-as-judge
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: pydantic>=2.7
|
|
22
|
+
Requires-Dist: pyyaml>=6
|
|
23
|
+
Provides-Extra: agents
|
|
24
|
+
Requires-Dist: duckdb>=1.1; extra == 'agents'
|
|
25
|
+
Requires-Dist: pytz>=2026.6; extra == 'agents'
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: anthropic>=1; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: demo
|
|
29
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'demo'
|
|
30
|
+
Requires-Dist: streamlit>=1.40; extra == 'demo'
|
|
31
|
+
Provides-Extra: openai
|
|
32
|
+
Requires-Dist: openai>=2; extra == 'openai'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# EvalKit
|
|
36
|
+
|
|
37
|
+
<!-- evalkit:headline:start -->
|
|
38
|
+
<p align="center"><img src="https://raw.githubusercontent.com/biplob819/evalkit/main/docs/assets/headline.svg" alt="On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional." width="760"></p>
|
|
39
|
+
|
|
40
|
+
On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional.
|
|
41
|
+
|
|
42
|
+
[Full report](https://biplob819.github.io/evalkit/) · results in [`docs/results/`](https://github.com/biplob819/evalkit/tree/main/docs/results)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install 'evalkit-harness[anthropic,openai]'
|
|
46
|
+
evalkit init # scaffold a project and a three-case sample task
|
|
47
|
+
evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 --dry-run # estimate
|
|
48
|
+
evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 # needs keys in .env
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Questions from [PubMedQA](https://pubmedqa.github.io/) (Jin et al., 2019, MIT licence); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md). Research benchmark, not medical advice.
|
|
52
|
+
<!-- evalkit:headline:end -->
|
|
53
|
+
|
|
54
|
+
Evaluate **your own task** across Claude and OpenAI models: accuracy, groundedness, citation validity, cost and latency side by side, with confidence intervals and two cross-family judges whose error rate is measured against held-out hand labels.
|
|
55
|
+
|
|
56
|
+
> **Status: v0.1.0, the first public release.** To work on EvalKit itself, clone it and run `uv sync --all-extras`.
|
|
57
|
+
|
|
58
|
+
**Start here:** [Quickstart](https://github.com/biplob819/evalkit/blob/main/docs/quickstart.md) (a first result in under five minutes) · [Write your own task](https://github.com/biplob819/evalkit/blob/main/docs/writing-a-task.md) (JSONL or CSV) · [Control cost](https://github.com/biplob819/evalkit/blob/main/docs/cost.md) · [Calibrate the judges](https://github.com/biplob819/evalkit/blob/main/docs/calibrate-your-task.md) · [Scorers and adapters](https://github.com/biplob819/evalkit/blob/main/docs/plugins.md) · [Run it in CI](https://github.com/biplob819/evalkit/blob/main/examples/github-action/evalkit.yml) · [Versioning](https://github.com/biplob819/evalkit/blob/main/docs/versioning.md)
|
|
59
|
+
|
|
60
|
+
Built-in tasks: `pubmedqa` (yes / no / maybe research questions) and `scifact` (supports / refutes / not enough info for scientific claims); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md).
|
|
61
|
+
|
|
62
|
+
No telemetry: data goes only to the providers you configure ([PRIVACY.md](https://github.com/biplob819/evalkit/blob/main/PRIVACY.md)). Security and key handling: [SECURITY.md](https://github.com/biplob819/evalkit/blob/main/SECURITY.md). Contributions welcome: [CONTRIBUTING.md](https://github.com/biplob819/evalkit/blob/main/CONTRIBUTING.md).
|
|
63
|
+
|
|
64
|
+
The package is published as `evalkit-harness` (the name `evalkit` on PyPI belongs to an unrelated project). The command and the import are both `evalkit`.
|
|
65
|
+
|
|
66
|
+
- Product: [EvalKit PRD](https://github.com/biplob819/evalkit/blob/main/EvalKit%20PRD.md)
|
|
67
|
+
- Design: [EvalKit Tech Spec](https://github.com/biplob819/evalkit/blob/main/EvalKit%20Tech%20Spec.md)
|
|
68
|
+
- Build status: `uv run python scripts/status.py`
|
|
69
|
+
|
|
70
|
+
MIT licence.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# EvalKit
|
|
2
|
+
|
|
3
|
+
<!-- evalkit:headline:start -->
|
|
4
|
+
<p align="center"><img src="https://raw.githubusercontent.com/biplob819/evalkit/main/docs/assets/headline.svg" alt="On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional." width="760"></p>
|
|
5
|
+
|
|
6
|
+
On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional.
|
|
7
|
+
|
|
8
|
+
[Full report](https://biplob819.github.io/evalkit/) · results in [`docs/results/`](https://github.com/biplob819/evalkit/tree/main/docs/results)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install 'evalkit-harness[anthropic,openai]'
|
|
12
|
+
evalkit init # scaffold a project and a three-case sample task
|
|
13
|
+
evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 --dry-run # estimate
|
|
14
|
+
evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 # needs keys in .env
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Questions from [PubMedQA](https://pubmedqa.github.io/) (Jin et al., 2019, MIT licence); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md). Research benchmark, not medical advice.
|
|
18
|
+
<!-- evalkit:headline:end -->
|
|
19
|
+
|
|
20
|
+
Evaluate **your own task** across Claude and OpenAI models: accuracy, groundedness, citation validity, cost and latency side by side, with confidence intervals and two cross-family judges whose error rate is measured against held-out hand labels.
|
|
21
|
+
|
|
22
|
+
> **Status: v0.1.0, the first public release.** To work on EvalKit itself, clone it and run `uv sync --all-extras`.
|
|
23
|
+
|
|
24
|
+
**Start here:** [Quickstart](https://github.com/biplob819/evalkit/blob/main/docs/quickstart.md) (a first result in under five minutes) · [Write your own task](https://github.com/biplob819/evalkit/blob/main/docs/writing-a-task.md) (JSONL or CSV) · [Control cost](https://github.com/biplob819/evalkit/blob/main/docs/cost.md) · [Calibrate the judges](https://github.com/biplob819/evalkit/blob/main/docs/calibrate-your-task.md) · [Scorers and adapters](https://github.com/biplob819/evalkit/blob/main/docs/plugins.md) · [Run it in CI](https://github.com/biplob819/evalkit/blob/main/examples/github-action/evalkit.yml) · [Versioning](https://github.com/biplob819/evalkit/blob/main/docs/versioning.md)
|
|
25
|
+
|
|
26
|
+
Built-in tasks: `pubmedqa` (yes / no / maybe research questions) and `scifact` (supports / refutes / not enough info for scientific claims); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md).
|
|
27
|
+
|
|
28
|
+
No telemetry: data goes only to the providers you configure ([PRIVACY.md](https://github.com/biplob819/evalkit/blob/main/PRIVACY.md)). Security and key handling: [SECURITY.md](https://github.com/biplob819/evalkit/blob/main/SECURITY.md). Contributions welcome: [CONTRIBUTING.md](https://github.com/biplob819/evalkit/blob/main/CONTRIBUTING.md).
|
|
29
|
+
|
|
30
|
+
The package is published as `evalkit-harness` (the name `evalkit` on PyPI belongs to an unrelated project). The command and the import are both `evalkit`.
|
|
31
|
+
|
|
32
|
+
- Product: [EvalKit PRD](https://github.com/biplob819/evalkit/blob/main/EvalKit%20PRD.md)
|
|
33
|
+
- Design: [EvalKit Tech Spec](https://github.com/biplob819/evalkit/blob/main/EvalKit%20Tech%20Spec.md)
|
|
34
|
+
- Build status: `uv run python scripts/status.py`
|
|
35
|
+
|
|
36
|
+
MIT licence.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format: `[TYPE] [NODE] — summary`. Types: FEAT, FIX, DOCS, HARNESS, RELEASE.
|
|
4
|
+
|
|
5
|
+
## 2026-11-15
|
|
6
|
+
- [RELEASE] [B2.5] — v0.1.0 prepared: SciFact as a second built-in task, CSV datasets, quickstart / task / plugin / cost / versioning docs, community files, privacy statement, CI action example, public API contract test, post-release smoke job on three OSes
|
|
7
|
+
- [FEAT] [D3.2] — demo page (Streamlit): replay by default with six prepared samples (`evalkit demo-cache`, $0.99 in three runs), live mode on the owner's keys behind the passcode or on a visitor's own keys (D-58), cards that fill in per model, aggregate-only logging; failed or unchecked cards are re-answered by the next cache run
|
|
8
|
+
- [FEAT] [D3.1] — see commit 20c2fbc
|
|
9
|
+
- [FEAT] [B2.3] — README headline chart and sentences generated from the published 200-case run (re-judged, D-52); report deployed to GitHub Pages by workflow (D-47, D-57)
|
|
10
|
+
- [FIX] [C3.4] — first full agent run (300 trials, $3.81): agent analytics answers may carry extra columns (D-55); one simulator view per simulator, not per task; held-out report labels; task an-06 month format. Result: Sonnet 81% vs Sol 82–83% public success, 75% each held-out, no separated gap; agents rarely asked clarifying questions (6 and 16 of 60 ambiguous trials)
|
|
11
|
+
- [FEAT] [D3.1] — demo backend: `compare()` answer cards (live single-judge path with per-model stage timeouts, replay), PubMed fetch by PMID, in-memory store for visitor data, guard (passcode, session rate limit, per-request cap) and a Postgres daily spend counter with atomic reservation (D-53, D-54)
|
|
12
|
+
- [FIX] [C3.4] — agent history sent as conversation turns so provider caches hit (pilot: Sonnet 66% cached, cost per trial $0.040 → $0.018); estimate uses measured pilot tokens (full run $6.16, was $31.52 on placeholders)
|
|
13
|
+
- [FIX] [A2.4] — groundedness now judges each claim against all sentences its answer cited; the extractor's per-claim mapping was a positional guess made without the sentence texts (88% of sampled 'unsupported' claims were supported). The first 200-case headline was withdrawn and the run re-judged (D-52, L-37)
|
|
14
|
+
|
|
15
|
+
## 2026-11-14
|
|
16
|
+
- [FEAT] [C3.4] — agent scorers (sql_match / attack outcome, violations, steps over minimum, recovery, cost per success), two-level bootstrap with per-simulator views, Holm-adjusted separation matrix, held-out gap, `evalkit agent-run` with estimate, total cap, resume and an Agents report; agent prompts made cache-friendly (Anthropic automatic caching) (D-51)
|
|
17
|
+
- [FEAT] [C3.3] — simulated user on Haiku 5.5 and Luna: cached structured replies from the hidden intent only, deterministic answer guard, simulator recorded on trajectories; knowledge-based answer guard; live B9 check 26/26 PASS with a stricter audit detector (3 live runs, $0.0040) (D-50)
|
|
18
|
+
- [FEAT] [C3.2] — agent sandbox and loop: per-trial read-only DuckDB copy with reset hash check (void trials), locked-down SQL, tool allow-list with argument schemas, one structured-output loop for Claude and GPT agents with step budget, cached turns for free replay (D-49)
|
|
19
|
+
- [FEAT] [C3.1] — agent suite: seeded Riverbend Outfitters DuckDB (optional `agents` extra, pinned content hash), 32 public tasks (16 analytics incl. 8 ambiguous with hidden intent, 16 prompt-injection attacks whose canaries sit in `system_settings`, so quoting an injection is safe and only obeying it leaks), 8 held-out tasks kept off the repo with a committed manifest, loader that re-checks every gold answer against its SQL (D-48)
|
|
20
|
+
- [FIX] [W0.1] — Windows CI: explicit UTF-8 on every text read/write (enforced by an AST test), UTF-8 console output, platform paths in tests (L-32)
|
|
21
|
+
- [FIX] [S2.4] — judge every claim against all evidence (not the extractor's citations); pending claims reported separately; live check re-run: PASS
|
|
22
|
+
- [FEAT] [S2.4] — explanation faithfulness scorer reusing the pinned extractor and blind judges (live or batch, cached), fixture, live check script (both judges match the fixture, $0.027)
|
|
23
|
+
- [FIX] [B2.4] — never overwrite user files (example output, task templates); doctor and run check SDKs and the keys the config needs; README installs the provider extras
|
|
24
|
+
- [FEAT] [B2.4] — onboarding: `evalkit init` (config, .env.example, task template, .gitignore; never overwrites), `evalkit doctor` (Python, SDKs, key names, network, prices age, cache), `evalkit report --example` (bundled example run, no keys or network), caveats in every run/report output; shared outputs written 0644
|
|
25
|
+
- [FEAT] [B2.3] — README headline chart and generated sentences (`evalkit.report.readme`), `scripts/publish_results.py`, GitHub Pages workflow (OWNER D-47)
|
|
26
|
+
- [FIX] [A2.4] — batch holds use 1.5x measured expected output, not the 4,096-token cap (a 200-case run could never start under $20); judges run concurrently (D-46)
|
|
27
|
+
- [FIX] [N1.2] — cost model from the 12-case real run: judge calls per claim (4.5 per answer), measured token assumptions; 200-case estimate $14.13 (D-46)
|
|
28
|
+
- [FIX] [B2.2] — run safety (D-45): total cost cap across resumes, judge-set lock per run folder, no live re-pay of open batches, dry run without keys, exit 2 with masked messages for every unexpected error
|
|
29
|
+
- [FEAT] [B2.2] — CLI: `evalkit run` (resumable run folders, dry run, cost cap exit 3, judging-pending exit 4, `--live-judges` sharing the batch cache, `--json`), `evalkit calibrate export|label|import|score`, `evalkit report [--publish]`; validation before any call; user YAML tasks with local JSONL datasets
|
|
30
|
+
- [FIX] [A2.5] — a pending run whose records carry no per-judge scores now aggregates (found by the CLI tests)
|
|
31
|
+
- [FEAT] [B2.1] — run report: one self-contained HTML file (D-42) with an accessible SVG quality-vs-cost scatter (CI whiskers, judge dodge, collision-free labels, log scale), model table with PASS/FAIL badges, Holm-labelled pairwise table, failure explorer, calibration and self-preference panels, research banner and caveat stamps; `charts.vl.json` alongside; golden report
|
|
32
|
+
- [FIX] [A2.5] — float totals use `math.fsum` so `summary.json` is identical on Python 3.11 and 3.12 (CI failure)
|
|
33
|
+
- [HARNESS] [W0.1] — golden gate also runs on the minimum supported Python (L-27)
|
|
34
|
+
- [FEAT] [A2.6] — calibration: stratified 100-answer sample (50 tuning / 50 held-out, 20 blind relabel), blind sheet and CSV, local keyboard labelling page (D-40), cluster-bootstrap kappa per judge and family, extraction error rate, self-consistency, rubric-bound `CalibrationReport`, kappa regression gate, publish guard, `docs/calibrate-your-task.md`
|
|
35
|
+
- [FEAT] [A2.5] — aggregate: per-judge headline (correct and fully grounded) over all cases, seeded pure-Python bootstrap CIs, paired comparisons with Holm (D-36), thresholds, error accounting, self-preference, derived stamps, atomic `summary.json`; golden end-to-end run
|
|
36
|
+
- [FEAT] [A2.4] — dual-judge batch: blind rubric with `judge_prompt_hash`, per-claim batch bookkeeping (resume never pays twice, expired items resubmitted), budget holds across resume, verdict/evidence/quote consistency, one live retry, cross-family dual mode
|
|
37
|
+
- [FIX] [A2.4] — four verifier passes; judge blinding per D-38 (no clinical false positives on PQA-L), collect bound covers downloads, per-judge lock
|
|
38
|
+
- [FEAT] [A2.4] — `scripts/planted_check.py`: live B6 check with positive controls; both judges pass
|
|
39
|
+
- [DOCS] [D-34] — judges and extractor at medium effort, NFR2 $20 per 200-case run; D-35..D-38 recorded
|
|
40
|
+
|
|
41
|
+
- [HARNESS] [W0.1] — Repo skeleton (`evalkit-harness`, src layout, Python ≥3.11), CI and release workflows, build graph (26 nodes) and capability catalog (144 rows), `verify_graph_eligibility.py`, `node_start.py`, `node_converge.py` (12 gates), `secret_scan.py`, `status.py`, every gate with a self-test, decision log seeded D-1 to D-14.
|
|
42
|
+
- [FIX] [W0.1] — Verifier findings: AST-checked proofs under tests/, git-diff errors fail gate 11, check-only node-start writes nothing, self-tests require the defect's identity, wider secret patterns, strict graph and catalog parsers.
|
|
43
|
+
- [HARNESS] [W0.1] — Green. HAR-08 (PyPI name) moved to B2.5 (OWNER D-20).
|
|
44
|
+
- [FEAT] [N1.1] — `evalkit.schemas` (results format v1: benchmark, summary, calibration, config, agent), `evalkit.config` YAML loaders with file-and-field errors, v1 fixtures and round-trip contract tests.
|
|
45
|
+
- [FIX] [N1.1] — Verifier findings: finite floats, required well-formed schema_version, honest RunSummary stamps, deep immutability, strict ints, linked claims and verdicts, loader errors that name the file, duplicate-key YAML rejected.
|
|
46
|
+
- [FEAT] [N1.1] — Green.
|
|
47
|
+
- [FEAT] [N1.3] — `evalkit.runtime`: typed Node contract, canonical-JSON cache keys, content-addressed store with lineage and batch-id meta, async executor (per-provider concurrency, fan-out in order, resume from cache, spend cap counting in-flight estimates), single-writer JSONL, code-version manifest check.
|
|
48
|
+
- [FEAT] [N1.4] — PubMedQA PQA-L loader (pinned checksum, 1,000 rows, cached, `long_answer` dropped), rule-based sentence splitter checked on all 1,000 abstracts, seeded label-stratified sampler, built-in `pubmedqa` task and prompt rendering, `DATA_CARD.md` with the contamination probe spec.
|
|
49
|
+
- [FIX] [N1.3] — Verifier findings: money values must be finite and non-negative, budget reserved inside the concurrency slot, identical in-flight work coalesced, batch ids reused via `submit_once`, corrupt cache entries recomputed, version manifest keyed by node name and hashing MRO plus schemas, writer refuses NaN and late writes.
|
|
50
|
+
- [FEAT] [N1.3] — Green.
|
|
51
|
+
- [FIX] [N1.4] — Verifier findings: splitter recovers 41 wrong merges (initials, units, unclosed brackets, spaced decimals), download verified before an atomic write, network failures raise DatasetError, duplicate ids rejected, data card claims made true or dated, probe spec fully pinned, PubMedQA credited in the README.
|
|
52
|
+
- [FEAT] [N1.4] — Green.
|
|
53
|
+
- [FEAT] [N1.2] — Adapters: Anthropic (Messages, Message Batches) and OpenAI (Chat Completions, Batch) plus OpenAI-compatible servers, behind one interface with entry-point plugins; no sampling params, explicit effort, structured JSON; `prices.yaml` (Claude 5.5 family, gpt-6.1-sol, gpt-6-luna, gpt-6.1-terra), per-call cost, pre-run estimate and reconciliation; retry policy (6 attempts, full jitter, transport errors only); token-bucket pacing.
|
|
54
|
+
- [FIX] [N1.2] — Verifier findings: expired/cancelled batches keep billed results, real SDK error types, damaged batch lines tolerated, duplicate custom_ids refused, explicit effort enforced, token bucket wired into every adapter call, plugin failures isolated, compatible-server URL and key checks, long-context price tiers, judges at low effort (OWNER D-28, estimate $10.35).
|
|
55
|
+
- [FEAT] [N1.2] — Green. Wave 1 (N1.1-N1.4) complete.
|
|
56
|
+
- [FEAT] [A2.2] — `evalkit.scorers`: Scorer protocol, registry and isolated entry-point plugins; label_correct, citation_valid, cost and latency (errors count as wrong).
|
|
57
|
+
- [FIX] [A2.2] — Verifier findings: built-in scorers can never be replaced by plugins, duplicate and mismatched plugin names warn, failed plugins load once, Unicode-normalised labels, strict finite scores, picklable scorers.
|
|
58
|
+
- [FEAT] [A2.1] — Generate and Validate nodes: provider routing, per-provider pools, structured answers with a strict parser, error taxonomy (transport not cached, refusal, schema never retried), `Executor.map_settled`, per-model effort in RunConfig, reference `configs/pubmedqa.yaml`, smoke-run module and script.
|
|
59
|
+
- [FEAT] [A2.2] — Green.
|
|
60
|
+
- [FEAT] [S2.1] — `sql_match`: result-set comparison (column reordering by name, multiset rows, ordered option, numeric tolerance, strict booleans/None/strings) with 21 golden cases.
|
|
61
|
+
- [FIX] [A2.1] — Verifier findings: budget exhaustion and configuration errors stop the run (only transient provider errors are 'transport'), answers must be one JSON object (validate v2), .env parsing, smoke exit codes and labels, tighter OpenAI routing.
|
|
62
|
+
- [FEAT] [S2.2] — Retrieval metrics: recall@k, MRR, nDCG@k (binary or graded), positions as returned, duplicates earn no credit.
|
|
63
|
+
- [FEAT] [A2.1] — Green (real smoke run pending API keys).
|
|
64
|
+
- [FEAT] [A2.3] — Claim extractor: pinned prompt with hashed extractor_version, ExtractClaims node at low effort with structured output, B11 checks (no invented citations, no duplicates, no empty extraction), coverage v1 (D-31).
|
|
65
|
+
- [FIX] [S2.1] — Verifier blocker fixed: unordered matching honours the tolerance exactly (exact-first, grouped, Hopcroft-Karp matching, windowed candidates), exact big-integer comparison, Decimal as numbers, 20k noisy rows < 3 s, brute-force fuzz.
|
|
66
|
+
- [FIX] [S2.2] — Any negative gain raises, all-zero gains undefined, string inputs rejected.
|
|
67
|
+
- [FEAT] [S2.1] — Green.
|
|
68
|
+
- [FEAT] [S2.2] — Green.
|
|
69
|
+
- [FIX] [A2.3] — Verifier findings: extractor v2 (version stamped on run, stale versions rejected), prompts hashed by the version check (manifest rebuilt for the new hash format, no behaviour change to generate/validate), per-item transport errors, colon-free case ids, B11 add/drop guard (D-32), coverage as a ScoreResult with details, aligned ClaimSet.
|
|
70
|
+
- [FEAT] [A2.3] — Green.
|
|
71
|
+
- [FEAT] [A2.4] — Dual judging: rubric v1 with its hash in every cache key, blind prompts with only the cited sentences (audit test), B3 precheck without calls, one batch per judge submitted once with per-claim caching, pending status, batch spend under the cost cap (Executor.reserve/settle), strict verdict parsing with counted exclusions, groundedness, 10 planted claims, single/dual judge selection.
|
|
72
|
+
- [FEAT] [S2.3] — `attack_outcome`: blocked / partial / succeeded from what the agent did (canaries in answer or nested tool arguments, disallowed tools); AttackBlocked gains `canaries` (additive).
|
|
73
|
+
- [FEAT] [S2.3] — Green.
|
|
74
|
+
- [MEASURE] [A2.1] — Real smoke run (20 cases × 4 models): 80/80 succeeded, $0.2418 spent; measured tokens put a 200-case run at $8.41. Spend ledger started at docs/measurements/spend-ledger.jsonl.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "evalkit-harness"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Evaluate your own task across Claude and OpenAI models: grounded, calibrated, cost-aware, with confidence intervals."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Biplob Chakraborty" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic>=2.7",
|
|
12
|
+
"pyyaml>=6",
|
|
13
|
+
]
|
|
14
|
+
keywords = ["llm", "evaluation", "llm-as-judge", "groundedness", "benchmark", "agents"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/biplob819/evalkit"
|
|
29
|
+
Issues = "https://github.com/biplob819/evalkit/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
evalkit = "evalkit.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
anthropic = [
|
|
36
|
+
"anthropic>=1",
|
|
37
|
+
]
|
|
38
|
+
openai = [
|
|
39
|
+
"openai>=2",
|
|
40
|
+
]
|
|
41
|
+
agents = [
|
|
42
|
+
"duckdb>=1.1",
|
|
43
|
+
"pytz>=2026.6",
|
|
44
|
+
]
|
|
45
|
+
demo = [
|
|
46
|
+
"psycopg[binary]>=3.2",
|
|
47
|
+
"streamlit>=1.40",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=8",
|
|
53
|
+
"ruff>=0.6",
|
|
54
|
+
"pyright>=1.1.380",
|
|
55
|
+
"types-pyyaml>=6.0.12.20261113",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["hatchling>=1.26"]
|
|
60
|
+
build-backend = "hatchling.build"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["src/evalkit"]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.build.targets.sdist]
|
|
66
|
+
# ship the library only; the build harness, specs and design docs live in the repo
|
|
67
|
+
include = ["src/evalkit", "README.md", "LICENSE", "changelog.md"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 100
|
|
71
|
+
target-version = "py311"
|
|
72
|
+
src = ["src", "scripts", "tests"]
|
|
73
|
+
extend-exclude = ["*.md"] # docs code blocks are illustrative; keep their hand alignment
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
|
|
77
|
+
ignore = ["RUF001", "RUF002", "RUF003"] # allow the › separator used in proof cells
|
|
78
|
+
|
|
79
|
+
[tool.pyright]
|
|
80
|
+
include = ["src", "scripts", "tests", "demo"]
|
|
81
|
+
pythonVersion = "3.11"
|
|
82
|
+
typeCheckingMode = "standard"
|
|
83
|
+
extraPaths = ["scripts"]
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
testpaths = ["tests"]
|
|
87
|
+
pythonpath = ["scripts"]
|
|
88
|
+
markers = [
|
|
89
|
+
"contract: schema, CLI and public API contract tests",
|
|
90
|
+
"golden: end-to-end run against recorded responses",
|
|
91
|
+
"judge: judge regression set",
|
|
92
|
+
"safety: secrets, blinding and guard tests",
|
|
93
|
+
"onboarding: no-key onboarding flow",
|
|
94
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Provider adapters. Import providers lazily: the SDKs are optional extras."""
|
|
2
|
+
|
|
3
|
+
from evalkit.adapters.base import (
|
|
4
|
+
ALLOWED_PARAMS,
|
|
5
|
+
BatchRequest,
|
|
6
|
+
BatchResult,
|
|
7
|
+
CompletionRequest,
|
|
8
|
+
ModelAdapter,
|
|
9
|
+
RawCompletion,
|
|
10
|
+
discover,
|
|
11
|
+
get_adapter,
|
|
12
|
+
register,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ALLOWED_PARAMS",
|
|
17
|
+
"BatchRequest",
|
|
18
|
+
"BatchResult",
|
|
19
|
+
"CompletionRequest",
|
|
20
|
+
"ModelAdapter",
|
|
21
|
+
"RawCompletion",
|
|
22
|
+
"discover",
|
|
23
|
+
"get_adapter",
|
|
24
|
+
"register",
|
|
25
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Anthropic Messages and Message Batches adapter (ADP-01, ADP-03).
|
|
2
|
+
|
|
3
|
+
- No sampling parameters (D-22), no refusal fallbacks (D-23), effort set explicitly from
|
|
4
|
+
request params (D-24), JSON via `output_config.format` (D-24).
|
|
5
|
+
- `output_tokens` includes thinking; thinking is reported separately as tokens_reasoning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from evalkit.adapters.base import (
|
|
16
|
+
BatchRequest,
|
|
17
|
+
BatchResult,
|
|
18
|
+
CompletionRequest,
|
|
19
|
+
ModelAdapter,
|
|
20
|
+
RawCompletion,
|
|
21
|
+
check_params,
|
|
22
|
+
check_unique_ids,
|
|
23
|
+
register,
|
|
24
|
+
)
|
|
25
|
+
from evalkit.cost import Price, call_cost, load_prices, price_for
|
|
26
|
+
from evalkit.ratelimit import TokenBucket
|
|
27
|
+
from evalkit.retry import with_retries
|
|
28
|
+
from evalkit.runtime.canonical import to_jsonable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register("anthropic")
|
|
32
|
+
class AnthropicAdapter(ModelAdapter):
|
|
33
|
+
provider = "anthropic"
|
|
34
|
+
|
|
35
|
+
require_effort = True
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
client: Any = None,
|
|
40
|
+
prices: dict[str, Price] | None = None,
|
|
41
|
+
bucket: TokenBucket | None = None,
|
|
42
|
+
**retry: Any,
|
|
43
|
+
):
|
|
44
|
+
self._client = client
|
|
45
|
+
self._prices = prices if prices is not None else load_prices()
|
|
46
|
+
self._bucket = bucket # optional requests-per-second pacing (ADP-07)
|
|
47
|
+
self._retry = retry
|
|
48
|
+
|
|
49
|
+
async def _paced(self, call: Any) -> Any:
|
|
50
|
+
if self._bucket is not None:
|
|
51
|
+
await self._bucket.acquire()
|
|
52
|
+
return await with_retries(call, **self._retry)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def client(self) -> Any:
|
|
56
|
+
if self._client is None:
|
|
57
|
+
import anthropic # optional dependency, imported only when used
|
|
58
|
+
|
|
59
|
+
self._client = anthropic.AsyncAnthropic(max_retries=0)
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
def build_params(self, req: CompletionRequest) -> dict[str, Any]:
|
|
63
|
+
check_params(req, require_effort=self.require_effort)
|
|
64
|
+
params: dict[str, Any] = {
|
|
65
|
+
"model": req.model_id,
|
|
66
|
+
"max_tokens": req.max_output_tokens,
|
|
67
|
+
"messages": [{"role": "user", "content": req.prompt}],
|
|
68
|
+
}
|
|
69
|
+
if req.turns:
|
|
70
|
+
params["messages"] = [{"role": r, "content": t} for r, t in req.turns]
|
|
71
|
+
if req.cache:
|
|
72
|
+
# automatic caching: the breakpoint follows the last block as the history grows
|
|
73
|
+
params["cache_control"] = {"type": "ephemeral"}
|
|
74
|
+
if req.system:
|
|
75
|
+
params["system"] = req.system
|
|
76
|
+
output_config: dict[str, Any] = {}
|
|
77
|
+
if "effort" in req.params:
|
|
78
|
+
output_config["effort"] = req.params["effort"]
|
|
79
|
+
if req.json_schema is not None:
|
|
80
|
+
output_config["format"] = {
|
|
81
|
+
"type": "json_schema",
|
|
82
|
+
"schema": to_jsonable(req.json_schema),
|
|
83
|
+
}
|
|
84
|
+
if output_config:
|
|
85
|
+
params["output_config"] = output_config
|
|
86
|
+
return params
|
|
87
|
+
|
|
88
|
+
def parse(
|
|
89
|
+
self, message: Any, req: CompletionRequest, latency_ms: int, *, batch: bool = False
|
|
90
|
+
) -> RawCompletion:
|
|
91
|
+
text = "".join(b.text for b in message.content if getattr(b, "type", "") == "text")
|
|
92
|
+
u = message.usage
|
|
93
|
+
cache_read = getattr(u, "cache_read_input_tokens", 0) or 0
|
|
94
|
+
cache_write = getattr(u, "cache_creation_input_tokens", 0) or 0
|
|
95
|
+
details = getattr(u, "output_tokens_details", None)
|
|
96
|
+
thinking = (getattr(details, "thinking_tokens", 0) or 0) if details else 0
|
|
97
|
+
thinking = min(thinking, u.output_tokens) # never bill more reasoning than output
|
|
98
|
+
tokens_in = u.input_tokens + cache_read + cache_write
|
|
99
|
+
tokens_out = max(u.output_tokens - thinking, 0)
|
|
100
|
+
cost = call_cost(
|
|
101
|
+
price_for(self._prices, req.model_id),
|
|
102
|
+
tokens_in=tokens_in,
|
|
103
|
+
tokens_out=tokens_out,
|
|
104
|
+
tokens_reasoning=thinking,
|
|
105
|
+
tokens_cached=cache_read,
|
|
106
|
+
tokens_cache_write=cache_write,
|
|
107
|
+
batch=batch,
|
|
108
|
+
)
|
|
109
|
+
stop = getattr(message, "stop_reason", "") or ""
|
|
110
|
+
return RawCompletion(
|
|
111
|
+
model=req.model,
|
|
112
|
+
model_snapshot=getattr(message, "model", "") or "",
|
|
113
|
+
params=dict(req.params),
|
|
114
|
+
raw_text=text,
|
|
115
|
+
tokens_in=tokens_in,
|
|
116
|
+
tokens_out=tokens_out,
|
|
117
|
+
tokens_reasoning=thinking,
|
|
118
|
+
tokens_cached=cache_read,
|
|
119
|
+
tokens_cache_write=cache_write,
|
|
120
|
+
latency_ms=latency_ms,
|
|
121
|
+
cost_usd=cost,
|
|
122
|
+
request_id=getattr(message, "_request_id", None) or getattr(message, "id", None),
|
|
123
|
+
stop_reason=stop,
|
|
124
|
+
refusal=stop == "refusal",
|
|
125
|
+
batch=batch,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def complete(self, req: CompletionRequest) -> RawCompletion:
|
|
129
|
+
params = self.build_params(req)
|
|
130
|
+
t0 = time.perf_counter()
|
|
131
|
+
message = await self._paced(lambda: self.client.messages.create(**params))
|
|
132
|
+
return self.parse(message, req, int((time.perf_counter() - t0) * 1000))
|
|
133
|
+
|
|
134
|
+
async def submit_batch(self, reqs: list[BatchRequest]) -> str:
|
|
135
|
+
check_unique_ids(reqs)
|
|
136
|
+
requests = [
|
|
137
|
+
{"custom_id": r.custom_id, "params": self.build_params(r.request)} for r in reqs
|
|
138
|
+
]
|
|
139
|
+
batch = await self._paced(lambda: self.client.messages.batches.create(requests=requests))
|
|
140
|
+
return str(batch.id)
|
|
141
|
+
|
|
142
|
+
async def collect_batch(
|
|
143
|
+
self, batch_id: str, reqs: Mapping[str, CompletionRequest]
|
|
144
|
+
) -> list[BatchResult] | None:
|
|
145
|
+
status = await with_retries(
|
|
146
|
+
lambda: self.client.messages.batches.retrieve(batch_id), **self._retry
|
|
147
|
+
)
|
|
148
|
+
if status.processing_status != "ended":
|
|
149
|
+
return None
|
|
150
|
+
results = self.client.messages.batches.results(batch_id)
|
|
151
|
+
if inspect.isawaitable(results):
|
|
152
|
+
results = await results
|
|
153
|
+
out: list[BatchResult] = []
|
|
154
|
+
seen: set[str] = set()
|
|
155
|
+
async for item in results:
|
|
156
|
+
cid = item.custom_id
|
|
157
|
+
seen.add(cid)
|
|
158
|
+
if cid not in reqs:
|
|
159
|
+
continue # not ours; results arrive in any order and are keyed by custom_id
|
|
160
|
+
kind = item.result.type
|
|
161
|
+
if kind == "succeeded":
|
|
162
|
+
out.append(
|
|
163
|
+
BatchResult(
|
|
164
|
+
custom_id=cid,
|
|
165
|
+
completion=self.parse(
|
|
166
|
+
item.result.message, reqs[cid], latency_ms=0, batch=True
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
err = getattr(item.result, "error", None)
|
|
172
|
+
# SDK shape: result.error is an ErrorResponse whose .error carries the type.
|
|
173
|
+
detail = getattr(getattr(err, "error", None), "type", None) or getattr(
|
|
174
|
+
err, "type", None
|
|
175
|
+
)
|
|
176
|
+
out.append(BatchResult(custom_id=cid, error=f"{kind}:{detail}" if detail else kind))
|
|
177
|
+
for cid in sorted(set(reqs) - seen):
|
|
178
|
+
out.append(BatchResult(custom_id=cid, error="missing_from_results"))
|
|
179
|
+
return out
|