evalcore 0.1.0__tar.gz → 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.
- {evalcore-0.1.0 → evalcore-0.2.0}/.github/workflows/ci.yml +13 -1
- evalcore-0.2.0/CHANGELOG.md +35 -0
- evalcore-0.1.0/README.md → evalcore-0.2.0/PKG-INFO +137 -77
- evalcore-0.1.0/PKG-INFO → evalcore-0.2.0/README.md +105 -102
- {evalcore-0.1.0 → evalcore-0.2.0}/docs/design.md +23 -23
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/README.md +7 -7
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/adapter.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/graders.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/run_eval.py +3 -3
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/suite.yaml +3 -3
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/tests/test_quickstart.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/pyproject.toml +16 -11
- evalcore-0.2.0/src/evalcore/__init__.py +88 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/__init__.py +3 -2
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/base.py +4 -3
- evalcore-0.1.0/src/evalkit/adapters/_env.py → evalcore-0.2.0/src/evalcore/adapters/env.py +11 -6
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/http.py +5 -5
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/replay.py +2 -2
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/cli.py +47 -23
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/compare.py +1 -1
- evalcore-0.2.0/src/evalcore/errors.py +19 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/__init__.py +7 -1
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/base.py +4 -3
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/classification.py +2 -2
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/deterministic.py +2 -2
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/judge.py +6 -9
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/numeric.py +3 -3
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/loader.py +2 -2
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/pairwise.py +4 -7
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/rating.py +10 -13
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/report.py +1 -1
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/__init__.py +10 -3
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/base.py +23 -10
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/html.py +59 -4
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/markdown.py +10 -4
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/runner.py +4 -4
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/store.py +1 -1
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/sweep.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_adapters.py +3 -3
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_cli.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_edge_cases.py +4 -4
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_judge.py +3 -3
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_judge_extra.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_live_clients.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_pairwise_extra.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_rating.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_rating_server.py +2 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_reporters.py +89 -2
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_retry.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_runner.py +3 -3
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_store.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_sweep_pairwise.py +1 -1
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_unit.py +5 -5
- evalcore-0.2.0/uv.lock +755 -0
- evalcore-0.1.0/src/evalkit/__init__.py +0 -39
- evalcore-0.1.0/uv.lock +0 -510
- {evalcore-0.1.0 → evalcore-0.2.0}/.github/workflows/publish.yml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/.gitignore +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/.pre-commit-config.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/LICENSE +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/__init__.py +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/__init__.py +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/tests/__init__.py +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/justfile +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/pyrightconfig.json +0 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/models.py +0 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/py.typed +0 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/refs.py +0 -0
- {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/retry.py +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/tests/__init__.py +0 -0
- {evalcore-0.1.0 → evalcore-0.2.0}/uv.toml +0 -0
|
@@ -10,13 +10,17 @@ on:
|
|
|
10
10
|
jobs:
|
|
11
11
|
test:
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
13
17
|
steps:
|
|
14
18
|
- uses: actions/checkout@v4
|
|
15
19
|
|
|
16
20
|
- name: Install uv
|
|
17
21
|
uses: astral-sh/setup-uv@v5
|
|
18
22
|
with:
|
|
19
|
-
python-version:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
20
24
|
|
|
21
25
|
- name: Sync dependencies
|
|
22
26
|
run: uv sync --all-extras
|
|
@@ -31,5 +35,13 @@ jobs:
|
|
|
31
35
|
uv run coverage run
|
|
32
36
|
uv run coverage report
|
|
33
37
|
|
|
38
|
+
build:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- name: Install uv
|
|
43
|
+
uses: astral-sh/setup-uv@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.14"
|
|
34
46
|
- name: Build wheel + sdist
|
|
35
47
|
run: uv build
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Breaking:** the import package and CLI are now `evalcore` (were `evalkit`).
|
|
11
|
+
Update `import evalkit` to `import evalcore` and the `evalkit` command to
|
|
12
|
+
`evalcore`. The distribution name (`evalcore`) is unchanged.
|
|
13
|
+
- Lowered the minimum Python to **3.11** (was 3.14).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `evalcore.__version__`.
|
|
17
|
+
- Public `evalcore.adapters.expand_env` for `${VAR}` expansion in custom
|
|
18
|
+
adapters (replaces the private `adapters._env` module).
|
|
19
|
+
- Exception hierarchy: `EvalcoreError` (base) and `ConfigError` (also a
|
|
20
|
+
`ValueError`, so existing handlers keep working).
|
|
21
|
+
- Top-level convenience entry points: `load_suite`, `load_cases`, `run_suite`,
|
|
22
|
+
`run_suite_sync`.
|
|
23
|
+
- HTML rendering for `sweep` and `pairwise` reports, and `--report` /
|
|
24
|
+
`--report-out` on those CLI commands.
|
|
25
|
+
|
|
26
|
+
## [0.1.0] - 2026-07-16
|
|
27
|
+
|
|
28
|
+
- Initial public release: adapters (http/replay), graders (deterministic,
|
|
29
|
+
numeric, classification, LLM judge + panel), runner (N-sampling, concurrency,
|
|
30
|
+
retries, checkpoint/resume), compare/gate, sweep, pairwise, blind human
|
|
31
|
+
rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
|
|
32
|
+
column-store outbox, and content-hash provenance.
|
|
33
|
+
|
|
34
|
+
[Unreleased]: https://github.com/scottpmiller/evalcore/compare/0.1.0...HEAD
|
|
35
|
+
[0.1.0]: https://github.com/scottpmiller/evalcore/releases/tag/0.1.0
|
|
@@ -1,14 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: evalcore
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
|
|
5
|
+
Project-URL: Homepage, https://github.com/scottpmiller/evalcore
|
|
6
|
+
Project-URL: Repository, https://github.com/scottpmiller/evalcore
|
|
7
|
+
Project-URL: Issues, https://github.com/scottpmiller/evalcore/issues
|
|
8
|
+
Author: AWeber Communications
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
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: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
25
|
+
Requires-Dist: pyyaml<7,>=6
|
|
26
|
+
Provides-Extra: http
|
|
27
|
+
Requires-Dist: httpx>=0.27; extra == 'http'
|
|
28
|
+
Provides-Extra: judge
|
|
29
|
+
Requires-Dist: anthropic>=0.40; extra == 'judge'
|
|
30
|
+
Requires-Dist: openai>=1.40; extra == 'judge'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# evalcore
|
|
2
34
|
|
|
3
35
|
A small, **consumer-agnostic** evaluation engine for prompt, model, and API
|
|
4
36
|
outputs. It detects regressions and improvements as prompts and models change,
|
|
5
37
|
by scoring a candidate against a baseline over a fixed dataset and applying
|
|
6
38
|
guardrails + a headline win metric to produce a gate verdict.
|
|
7
39
|
|
|
8
|
-
|
|
9
|
-
supplies four things;
|
|
40
|
+
evalcore knows nothing about any particular system under test. A consumer
|
|
41
|
+
supplies four things; evalcore supplies everything else:
|
|
10
42
|
|
|
11
|
-
| Consumer provides (data + small plug-ins) |
|
|
43
|
+
| Consumer provides (data + small plug-ins) | evalcore provides |
|
|
12
44
|
| --- | --- |
|
|
13
45
|
| an **adapter** config (how to call the system + the knobs a variant sets) | runner (N-sampling), comparison/regression engine |
|
|
14
46
|
| **datasets** (cases with opaque `input`/`expected` blobs) | grader registry + generic graders |
|
|
@@ -23,12 +55,38 @@ offline) and doubles as an end-to-end usage reference.
|
|
|
23
55
|
## Install
|
|
24
56
|
|
|
25
57
|
```bash
|
|
26
|
-
pip install evalcore
|
|
58
|
+
pip install evalcore
|
|
27
59
|
```
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
Imports as `evalcore` (`import evalcore`); CLI is `evalcore` (`evalcore --help`).
|
|
62
|
+
Extras: `evalcore[http]` (live HTTP adapter), `evalcore[judge]` (live LLM judge).
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
Score a candidate against a baseline over a suite and read the gate verdict -
|
|
67
|
+
fully offline against recorded fixtures, so it runs with no network or API keys:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import evalcore as ec
|
|
71
|
+
|
|
72
|
+
suite = ec.load_suite('suite.yaml')
|
|
73
|
+
baseline = ec.run_suite_sync(suite, 'baseline', mode='replay').scorecard
|
|
74
|
+
candidate = ec.run_suite_sync(suite, 'candidate', mode='replay').scorecard
|
|
75
|
+
|
|
76
|
+
result = ec.compare.compare(baseline, candidate, suite.thresholds)
|
|
77
|
+
print(result.verdict) # 'pass' | 'warn' | 'fail'
|
|
78
|
+
print(ec.report.render_comparison(result)) # Markdown diff + guardrails
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or from the command line (exits non-zero on `fail`, so it drops into CI):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
evalcore gate --suite suite.yaml --mode replay
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`examples/quickstart/` is a complete, runnable version of the above (custom
|
|
88
|
+
adapter + graders + suite). The rest of this README is the reference for
|
|
89
|
+
writing your own.
|
|
32
90
|
|
|
33
91
|
## Develop
|
|
34
92
|
|
|
@@ -69,7 +127,7 @@ The five steps, in dependency order:
|
|
|
69
127
|
## 1. Cases (the dataset)
|
|
70
128
|
|
|
71
129
|
A dataset is a directory containing `cases/`, holding one YAML (or JSON)
|
|
72
|
-
file per case. Every field except `id` is **opaque to the engine**
|
|
130
|
+
file per case. Every field except `id` is **opaque to the engine** - only
|
|
73
131
|
your adapter and graders interpret `input` and `expected`:
|
|
74
132
|
|
|
75
133
|
```yaml
|
|
@@ -99,7 +157,7 @@ class TargetAdapter(typing.Protocol):
|
|
|
99
157
|
```
|
|
100
158
|
|
|
101
159
|
It receives one `Case` and one `Variant` and must return an
|
|
102
|
-
`Output`
|
|
160
|
+
`Output` - never raise for a failed invocation; set `Output.error` instead
|
|
103
161
|
so graders can count it (see `errors` below). Set `Output.retryable = True`
|
|
104
162
|
alongside a *transient* error (a 429, a 5xx, a network timeout) and the
|
|
105
163
|
runner's retry loop will back off and try again (see "Retries" below); leave
|
|
@@ -137,15 +195,15 @@ the latency still recorded.
|
|
|
137
195
|
|
|
138
196
|
### Custom adapters
|
|
139
197
|
|
|
140
|
-
When the built-in isn't enough
|
|
141
|
-
non-HTTP targets
|
|
198
|
+
When the built-in isn't enough - auth dances, response post-processing,
|
|
199
|
+
non-HTTP targets - register your own under a config `type` and select it in
|
|
142
200
|
the suite. Subclassing the http adapter is often the shortest path.
|
|
143
201
|
`examples/quickstart/adapter.py` is a worked example: it turns each case's
|
|
144
202
|
input into structured `Output.fields` your graders can score:
|
|
145
203
|
|
|
146
204
|
```python
|
|
147
|
-
from
|
|
148
|
-
from
|
|
205
|
+
from evalcore import models
|
|
206
|
+
from evalcore.adapters import base, http
|
|
149
207
|
|
|
150
208
|
@base.register('my_service_json')
|
|
151
209
|
class MyAdapter(http.HTTPAdapter):
|
|
@@ -157,20 +215,20 @@ class MyAdapter(http.HTTPAdapter):
|
|
|
157
215
|
|
|
158
216
|
Constructor kwargs come from the suite's `adapter:` mapping (everything
|
|
159
217
|
except `type`). Load the module at run time with `--plugins my_service.graders`
|
|
160
|
-
(CLI) or a plain `import` (Python API)
|
|
218
|
+
(CLI) or a plain `import` (Python API) - registration happens on import.
|
|
161
219
|
|
|
162
220
|
An adapter need not be HTTP-backed: it can grade *what a deployed system
|
|
163
|
-
already did* by reading from an observability store
|
|
221
|
+
already did* by reading from an observability store - turning an aggregated
|
|
164
222
|
result row into `Output.fields`.
|
|
165
223
|
|
|
166
224
|
An adapter that holds resources (a browser, an injected session, pooled
|
|
167
225
|
connections) may expose an optional async `aclose()`; the runner calls it
|
|
168
|
-
after the run, even on failure
|
|
226
|
+
after the run, even on failure - so a browser-automation adapter (e.g. one
|
|
169
227
|
driving Playwright) can open its context once and tear it down cleanly.
|
|
170
228
|
|
|
171
229
|
## 3. Variants (what's being compared)
|
|
172
230
|
|
|
173
|
-
A variant is a named dict of **knobs**
|
|
231
|
+
A variant is a named dict of **knobs** - opaque to the engine, interpreted
|
|
174
232
|
by your adapter (usually via `$variant.*` refs in the body template):
|
|
175
233
|
|
|
176
234
|
```yaml
|
|
@@ -265,17 +323,17 @@ metrics: only `Score`s reach a scorecard, so a value the adapter merely put in
|
|
|
265
323
|
field's metric name defaults to the ref's leaf (`output.cost` -> `cost`); add
|
|
266
324
|
`min`/`max` to also emit per-case pass/fail. Absent or non-numeric fields
|
|
267
325
|
degrade to `null`. `compare`'s guardrails and a `win_metric` with
|
|
268
|
-
`win_higher_is_better: false` then gate on these directly
|
|
326
|
+
`win_higher_is_better: false` then gate on these directly - e.g. gating a
|
|
269
327
|
`generation_cost` or `tool_error_rate` alongside quality judges.
|
|
270
328
|
|
|
271
329
|
The judge runs live (`AnthropicJudgeClient` forced tool call, or
|
|
272
|
-
`OpenAIJudgeClient` `json_schema`
|
|
330
|
+
`OpenAIJudgeClient` `json_schema` - both temperature 0, needing the `judge`
|
|
273
331
|
extra plus `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`) or offline
|
|
274
332
|
(`ReplayJudgeClient`), chosen by the run mode like the adapter. Each
|
|
275
333
|
dimension becomes a metric `<name>.<key>` plus a `<name>.overall` mean.
|
|
276
334
|
|
|
277
335
|
**Panel + images.** Replace the single `model`/`replay_path` with a
|
|
278
|
-
`judges:` list to run a **panel**
|
|
336
|
+
`judges:` list to run a **panel** - each judge scores independently:
|
|
279
337
|
|
|
280
338
|
```yaml
|
|
281
339
|
- type: llm_judge
|
|
@@ -294,14 +352,14 @@ A panel emits, on top of the per-dimension panel means and `<name>.overall`:
|
|
|
294
352
|
`<name>.<judge>.overall` (each judge's own mean, so a systematically
|
|
295
353
|
generous judge is visible), `<name>.disagreement` (mean inter-judge spread
|
|
296
354
|
in raw points), and `<name>.flagged` (1.0 when any dimension's spread
|
|
297
|
-
reaches `disagreement_threshold`
|
|
355
|
+
reaches `disagreement_threshold` - averaged across cases, the fraction a
|
|
298
356
|
human should review). `image_refs` resolve to file paths (from
|
|
299
357
|
`output.artifacts`) or inline `{media_type, data}`; images are sent live
|
|
300
358
|
only, so replay stays offline. A single judge emits none of the panel-only
|
|
301
359
|
metrics, so existing single-judge suites are unchanged (a panel is the natural
|
|
302
360
|
fit for judging rendered screenshots with a Claude+GPT pair, for instance).
|
|
303
361
|
|
|
304
|
-
Custom graders register exactly like adapters
|
|
362
|
+
Custom graders register exactly like adapters - see
|
|
305
363
|
`examples/quickstart/graders.py` for one of each kind (a per-case
|
|
306
364
|
keyword check and a whole-run distinctness check). A grader that needs to
|
|
307
365
|
know the run mode (live vs replay) can expose `set_mode(mode: str)`; the
|
|
@@ -310,7 +368,7 @@ runner calls it before the run starts.
|
|
|
310
368
|
## 5. Fixtures (offline / CI runs)
|
|
311
369
|
|
|
312
370
|
Replay mode swaps the configured adapter for recorded outputs, keyed by
|
|
313
|
-
case id and variant name
|
|
371
|
+
case id and variant name - the whole pipeline then runs with no network,
|
|
314
372
|
keys, or deployed service:
|
|
315
373
|
|
|
316
374
|
```yaml
|
|
@@ -384,56 +442,58 @@ reported informationally.
|
|
|
384
442
|
|
|
385
443
|
```bash
|
|
386
444
|
# one variant -> scorecard (optionally saved)
|
|
387
|
-
|
|
445
|
+
evalcore --plugins my_service.graders run \
|
|
388
446
|
--suite my_service/suite.yaml --variant candidate --mode replay \
|
|
389
447
|
--out candidate.scorecard.json --revision "$GIT_SHA"
|
|
390
448
|
|
|
391
449
|
# the CI workhorse: run baseline+candidate, compare, exit 1 on 'fail'
|
|
392
|
-
|
|
450
|
+
evalcore --plugins my_service.graders gate \
|
|
393
451
|
--suite my_service/suite.yaml --mode replay \
|
|
394
452
|
--export outbox.jsonl --revision "$GIT_SHA"
|
|
395
453
|
|
|
396
454
|
# re-compare two previously saved runs (accepts --out scorecards OR
|
|
397
455
|
# --run-out run files)
|
|
398
|
-
|
|
456
|
+
evalcore compare --suite my_service/suite.yaml \
|
|
399
457
|
--baseline old.run.json --candidate new.run.json
|
|
400
458
|
```
|
|
401
459
|
|
|
402
460
|
`gate` picks variant names from `thresholds.variants`, falling back to
|
|
403
461
|
variants literally named `baseline`/`candidate`. `--revision` is an opaque
|
|
404
|
-
provenance id (git SHA, image digest, release label
|
|
462
|
+
provenance id (git SHA, image digest, release label - whatever your world
|
|
405
463
|
uses; the engine never interprets it).
|
|
406
464
|
|
|
407
|
-
`run`, `compare`, and `
|
|
408
|
-
`--report html` for a standalone, self-contained report document
|
|
409
|
-
artifact or PR attachment)
|
|
410
|
-
|
|
411
|
-
and select it by name,
|
|
465
|
+
`run`, `compare`, `gate`, `sweep`, and `pairwise` take `--report markdown`
|
|
466
|
+
(default) or `--report html` for a standalone, self-contained report document
|
|
467
|
+
(a CI artifact or PR attachment), plus `--report-out FILE` to write it instead
|
|
468
|
+
of printing. Reporters are a registry seam like adapters and graders - register
|
|
469
|
+
a custom format with `evalcore.reporters.base.register` and select it by name,
|
|
470
|
+
e.g. `--report pdf`. Every report type is also available as a plain library
|
|
471
|
+
call (`evalcore.reporters` / `evalcore.report`), returning the rendered string.
|
|
412
472
|
|
|
413
|
-
**The change loop
|
|
473
|
+
**The change loop - run, change, run, compare.** To measure whether a
|
|
414
474
|
change (a prompt edit, a new model, a frontend PR) helped or regressed,
|
|
415
475
|
run the *same variant* before and after and compare the two saved runs:
|
|
416
476
|
|
|
417
477
|
```bash
|
|
418
|
-
|
|
478
|
+
evalcore ... run --variant candidate --run-out before.run.json --revision before
|
|
419
479
|
# ... make the change (edit the prompt, point at the PR build, swap the model) ...
|
|
420
|
-
|
|
421
|
-
|
|
480
|
+
evalcore ... run --variant candidate --run-out after.run.json --revision after
|
|
481
|
+
evalcore compare --suite my_service/suite.yaml \
|
|
422
482
|
--baseline before.run.json --candidate after.run.json # deltas + verdict
|
|
423
483
|
```
|
|
424
484
|
|
|
425
485
|
The comparison's guardrails + win metric then read as "did the change
|
|
426
486
|
regress?" For nondeterministic targets (LLMs, browsers) raise `n_samples`
|
|
427
|
-
so each metric is a mean (with `stdev`) over several generations
|
|
487
|
+
so each metric is a mean (with `stdev`) over several generations - a single
|
|
428
488
|
run per side makes a small delta indistinguishable from run-to-run noise,
|
|
429
489
|
and `win_min_delta` is your noise floor.
|
|
430
490
|
|
|
431
|
-
**Python API**
|
|
491
|
+
**Python API** - everything the CLI does is a library call; the full worked
|
|
432
492
|
version is `examples/quickstart/run_eval.py`:
|
|
433
493
|
|
|
434
494
|
```python
|
|
435
495
|
import my_service.graders # noqa: F401 (registers custom types)
|
|
436
|
-
from
|
|
496
|
+
from evalcore import compare, loader, report, runner, store
|
|
437
497
|
|
|
438
498
|
suite = loader.load_suite('my_service/suite.yaml')
|
|
439
499
|
baseline = runner.run_suite_sync(suite, 'baseline', mode='replay',
|
|
@@ -479,8 +539,8 @@ observation passed; `0.9167` = 11 of 12. The metric name is the grader's
|
|
|
479
539
|
rubric-scored 1..scale by the pinned judge, normalized to 0..1, averaged
|
|
480
540
|
across observations. `overall` is the per-output mean of the dimensions,
|
|
481
541
|
then averaged. An output the judge couldn't score (errored invocation,
|
|
482
|
-
missing content) contributes *nothing*
|
|
483
|
-
counted as zero
|
|
542
|
+
missing content) contributes *nothing* - it is excluded from the mean, not
|
|
543
|
+
counted as zero - so always read judge means alongside `errors`.
|
|
484
544
|
|
|
485
545
|
**Set-level aggregates** (classification + custom aggregate graders).
|
|
486
546
|
Computed once over the whole run from a confusion matrix. The
|
|
@@ -497,7 +557,7 @@ stray verdict can never masquerade as a catch. Then:
|
|
|
497
557
|
| `f1` | 2PR/(P+R) | single-number balance of the two |
|
|
498
558
|
| `false_negative_rate` | FN/(FN+TP) | misses, as a fraction of real positives |
|
|
499
559
|
| `false_positive_rate` | FP/(FP+TN) | false alarms, as a fraction of real negatives |
|
|
500
|
-
| `accuracy` | (TP+TN)/all | overall fraction correct
|
|
560
|
+
| `accuracy` | (TP+TN)/all | overall fraction correct - flatters on imbalanced data; never guardrail it |
|
|
501
561
|
| `support_positive` / `support_negative` | TP+FN / TN+FP | the denominators: how much evidence backs the rates |
|
|
502
562
|
| `errors` | count | invocations that failed or produced no usable label |
|
|
503
563
|
|
|
@@ -507,7 +567,7 @@ slipped through", FPR = "a negative got blocked". They have different
|
|
|
507
567
|
denominators, so they stay honest on imbalanced datasets where `accuracy`
|
|
508
568
|
lies. `support_*` doubles as a drift alarm: if it changes between runs on
|
|
509
569
|
the same `dataset_hash`, extraction or labels broke. `errors` is a raw
|
|
510
|
-
count and worth a `max: 0` guardrail
|
|
570
|
+
count and worth a `max: 0` guardrail - errored results are excluded from
|
|
511
571
|
every rate, so without it a variant that crashes on its hardest cases
|
|
512
572
|
would look *better*.
|
|
513
573
|
|
|
@@ -526,7 +586,7 @@ is not the combined F1).
|
|
|
526
586
|
The badge is the verdict; the tail is why. The delta table lists every
|
|
527
587
|
metric with baseline / candidate / delta; the row marked
|
|
528
588
|
**(improved | regressed | neutral)** is the configured win metric. Its call
|
|
529
|
-
uses the dead band: `|delta| <= win_min_delta` → **neutral**
|
|
589
|
+
uses the dead band: `|delta| <= win_min_delta` → **neutral** - deliberate
|
|
530
590
|
protection against celebrating (or reverting on) noise from small samples.
|
|
531
591
|
|
|
532
592
|
The **Guardrails** section shows each rule as `[ok]` or `[BREACH]` with the
|
|
@@ -539,7 +599,7 @@ measured value. Verdict logic, in order:
|
|
|
539
599
|
|
|
540
600
|
`gate` (and the example driver) exit non-zero exactly on **FAIL**, so the
|
|
541
601
|
verdict drops straight into CI. A **PASS with neutral win** is a perfectly
|
|
542
|
-
good outcome
|
|
602
|
+
good outcome - it means "no regression, no proven improvement".
|
|
543
603
|
|
|
544
604
|
## Provenance (trusting a number later)
|
|
545
605
|
|
|
@@ -550,19 +610,19 @@ judge_version, revision, suite_hash, dataset_hash, mode, created_at`.
|
|
|
550
610
|
The declared versions state *intent*; the engine-computed content hashes
|
|
551
611
|
prove it: `suite_hash` digests the raw suite file, `dataset_hash` digests
|
|
552
612
|
the loaded cases (order-independent, formatting-independent). Two runs
|
|
553
|
-
whose hashes match evaluated the same config over the same data
|
|
613
|
+
whose hashes match evaluated the same config over the same data - if a
|
|
554
614
|
metric moved, the *system under test* moved. If a hash changed, the eval
|
|
555
615
|
itself changed and the comparison is apples-to-oranges: re-baseline.
|
|
556
616
|
`revision` ties the run to whatever provenance scheme you use (commit,
|
|
557
617
|
image digest, release label). A judge model/prompt/scale change is also a
|
|
558
|
-
re-baseline event
|
|
618
|
+
re-baseline event - bump `judge_version`; the runner lifts each judge
|
|
559
619
|
grader's pin (`key@version`, a panel joins them) onto `Scorecard.judge_version`
|
|
560
620
|
so it rides the reproducibility key, and it stays recorded on every judge
|
|
561
621
|
score's `detail` too.
|
|
562
622
|
|
|
563
623
|
## Per-sample results
|
|
564
624
|
|
|
565
|
-
`runner.run_suite` returns a **`RunResult`**
|
|
625
|
+
`runner.run_suite` returns a **`RunResult`** - the scorecard plus every
|
|
566
626
|
per-sample `CaseResult` (the output, its `artifacts`, and its scores). The
|
|
567
627
|
scorecard is the aggregate; the results are the ground truth it was folded
|
|
568
628
|
from. Persist the whole thing with `store.write_run` (CLI: `run --run-out`)
|
|
@@ -575,10 +635,10 @@ observations, so repeat-generation spread is visible, not just the average.
|
|
|
575
635
|
Set `concurrency: N` in the suite to run invocations concurrently (the
|
|
576
636
|
adapter and per-case graders must then tolerate concurrent calls).
|
|
577
637
|
|
|
578
|
-
**Retries.** Live targets fail transiently
|
|
638
|
+
**Retries.** Live targets fail transiently - a rate limit, a 5xx, a dropped
|
|
579
639
|
connection. A `retry:` block (above) makes the runner re-invoke the adapter
|
|
580
640
|
with exponential backoff (`backoff_base * 2**(attempt-1)`, capped at
|
|
581
|
-
`backoff_max`, ± `jitter`) when
|
|
641
|
+
`backoff_max`, ± `jitter`) when - and only when - the adapter marks the
|
|
582
642
|
failure `Output.retryable`. The built-in `http` adapter flags 429/5xx/network
|
|
583
643
|
errors and leaves other 4xx terminal; a custom adapter sets the flag for
|
|
584
644
|
whatever its transient failures are. The default (`max_attempts: 1`) is a
|
|
@@ -596,14 +656,14 @@ trail. Re-run with `--resume` and it reuses the recorded results and invokes
|
|
|
596
656
|
only what's missing, reusing the original `run_id`:
|
|
597
657
|
|
|
598
658
|
```bash
|
|
599
|
-
|
|
659
|
+
evalcore run --suite suite.yaml --variant candidate --checkpoint run.ckpt
|
|
600
660
|
# ... interrupted after 40/100 cases ...
|
|
601
|
-
|
|
661
|
+
evalcore run --suite suite.yaml --variant candidate --checkpoint run.ckpt --resume
|
|
602
662
|
```
|
|
603
663
|
|
|
604
664
|
The checkpoint's meta line records `suite_hash`/`dataset_hash`, so a resume
|
|
605
665
|
against a changed suite, dataset, or variant refuses rather than mixing
|
|
606
|
-
incompatible results
|
|
666
|
+
incompatible results - delete the checkpoint to start over (which `--resume`
|
|
607
667
|
also does implicitly when the file is absent). A checkpointed `(case, sample)`
|
|
608
668
|
is treated as done whether it succeeded or errored; to redo just the failures,
|
|
609
669
|
drop their lines from the checkpoint first.
|
|
@@ -611,29 +671,29 @@ drop their lines from the checkpoint first.
|
|
|
611
671
|
## Human rating & judge calibration
|
|
612
672
|
|
|
613
673
|
An LLM judge is only trustworthy as a win metric once you've checked it
|
|
614
|
-
tracks human taste.
|
|
674
|
+
tracks human taste. evalcore closes that loop over the persisted runs:
|
|
615
675
|
|
|
616
676
|
```bash
|
|
617
677
|
# blind rating web app over one or more saved runs (repeat --run to blind
|
|
618
678
|
# across variants: the browser never sees which model produced an output)
|
|
619
|
-
|
|
679
|
+
evalcore rate --run cand.run.json --run base.run.json \
|
|
620
680
|
--ratings ratings.jsonl --dimensions visual_design,copy_quality \
|
|
621
681
|
--content-ref output.html # or a screenshot via artifacts.*
|
|
622
682
|
|
|
623
683
|
# how well the judge agreed with the humans, per dimension
|
|
624
|
-
|
|
684
|
+
evalcore agreement --run cand.run.json --ratings ratings.jsonl \
|
|
625
685
|
--dimensions visual_design,copy_quality --judge-name quality
|
|
626
686
|
```
|
|
627
687
|
|
|
628
688
|
`rate` serves a dependency-free localhost page: a seeded-shuffled queue and
|
|
629
689
|
1..scale buttons per dimension. It renders each item as **typed panels**
|
|
630
|
-
derived from the output
|
|
631
|
-
with a rendered/source toggle), `json`, or `text`
|
|
690
|
+
derived from the output - `image`/`pdf` artifacts, `html` (sandboxed iframe
|
|
691
|
+
with a rendered/source toggle), `json`, or `text` - so plain-text
|
|
632
692
|
or JSON results render with zero config, and any number of artifacts become
|
|
633
693
|
that many panels. `--content-ref`/`--screenshot-ref` are the common
|
|
634
694
|
shorthand; a repeatable `--view label:kind:ref` gives explicit control.
|
|
635
695
|
Sessions are **resumable** (a rater only sees items they haven't scored).
|
|
636
|
-
**Blinding is enforced server-side**
|
|
696
|
+
**Blinding is enforced server-side** - the queue payload carries an opaque
|
|
637
697
|
item id and never the run/variant/model; ratings map back to
|
|
638
698
|
`(run_id, case_id, sample_idx)` only on the server. Ratings land in a JSONL
|
|
639
699
|
file (`models.Rating`) that is the **open interchange format**: any external
|
|
@@ -646,36 +706,36 @@ dimension as a win metric; a panel's `flagged` cases (see the judge panel
|
|
|
646
706
|
above) are the natural first items to route through `rate`.
|
|
647
707
|
|
|
648
708
|
**Side-by-side preference (`rank`/`preferences`).** `rate` scores each output
|
|
649
|
-
in isolation; `rank` is its A-vs-B analog
|
|
709
|
+
in isolation; `rank` is its A-vs-B analog - the human counterpart of
|
|
650
710
|
`pairwise`. It shows both variants' outputs for the same case as neutral
|
|
651
711
|
"Option 1"/"Option 2" columns and the rater picks a winner overall and per
|
|
652
712
|
dimension:
|
|
653
713
|
|
|
654
714
|
```bash
|
|
655
715
|
# blind side-by-side ranking web app over two saved runs
|
|
656
|
-
|
|
716
|
+
evalcore rank --run-a base.run.json --run-b cand.run.json \
|
|
657
717
|
--preferences prefs.jsonl --dimensions visual_design,copy_quality \
|
|
658
718
|
--content-ref output.html
|
|
659
719
|
|
|
660
720
|
# human A-vs-B win-rate (overall + per dimension) from the collected file
|
|
661
|
-
|
|
721
|
+
evalcore preferences --run-a base.run.json --run-b cand.run.json \
|
|
662
722
|
--preferences prefs.jsonl --report html --report-out prefs.html
|
|
663
723
|
```
|
|
664
724
|
|
|
665
725
|
Left/right sides are **shuffled per rater and un-blinded server-side**, so a
|
|
666
726
|
stored pick is always in *variant* terms (`variant_a`/`variant_b`) regardless
|
|
667
|
-
of which side it was shown on
|
|
727
|
+
of which side it was shown on - position bias counterbalances across raters
|
|
668
728
|
exactly like `pairwise`'s order swap. Sessions are resumable and picks land
|
|
669
729
|
in a JSONL file (`models.Preference`), the **open A-vs-B interchange format**.
|
|
670
730
|
`preferences` aggregates it (ties count half); to check the LLM pairwise judge
|
|
671
731
|
against the human panel, pass the same file to `pairwise --preferences
|
|
672
|
-
prefs.jsonl`, which appends a per-case human-vs-judge agreement table
|
|
732
|
+
prefs.jsonl`, which appends a per-case human-vs-judge agreement table - the
|
|
673
733
|
head-to-head calibration gate.
|
|
674
734
|
|
|
675
735
|
**Live judges on recorded data.** `--mode` drives the adapter; `--judge-mode`
|
|
676
736
|
drives the graders independently (default: same as `--mode`). So
|
|
677
737
|
`--mode replay --judge-mode live` re-scores recorded outputs with the live
|
|
678
|
-
judge panel
|
|
738
|
+
judge panel - no regeneration - which is how you iterate on a rubric or
|
|
679
739
|
re-record judge fixtures cheaply.
|
|
680
740
|
|
|
681
741
|
## Sweeps & pairwise win-rate
|
|
@@ -684,20 +744,20 @@ re-record judge fixtures cheaply.
|
|
|
684
744
|
|
|
685
745
|
```bash
|
|
686
746
|
# N-way: run every variant (or a subset) and rank them by the win metric
|
|
687
|
-
|
|
747
|
+
evalcore sweep --suite suite.yaml --mode replay # or --variants a,b,c
|
|
688
748
|
|
|
689
749
|
# A-vs-B: a judge picks a winner per case -> A's win-rate
|
|
690
|
-
|
|
750
|
+
evalcore pairwise --suite suite.yaml --a baseline --b candidate --mode replay
|
|
691
751
|
```
|
|
692
752
|
|
|
693
753
|
`sweep` prints a ranked leaderboard plus a full metric × variant matrix (a
|
|
694
|
-
model × prompt-version grid is just several named variants)
|
|
754
|
+
model × prompt-version grid is just several named variants) - reusing the
|
|
695
755
|
per-variant runner unchanged; it's pure orchestration + tabulation.
|
|
696
756
|
|
|
697
757
|
`pairwise` is the sharper subjective signal: instead of scoring each output
|
|
698
758
|
in isolation, a judge is shown **both** variants' outputs for the same case
|
|
699
|
-
and picks a winner, and
|
|
700
|
-
Order is **counterbalanced**
|
|
759
|
+
and picks a winner, and evalcore reports A's win-rate (ties count half).
|
|
760
|
+
Order is **counterbalanced** - each pair is judged both ways and a pick that
|
|
701
761
|
flips when you swap the order collapses to a tie, so position bias can't
|
|
702
762
|
manufacture a winner. Configure it under `thresholds.pairwise` (`content_ref`,
|
|
703
763
|
`model`/`replay_path`, optional `rubric`/`context_refs`); it runs live
|
|
@@ -738,16 +798,16 @@ Built-ins: `http` + `replay` adapters; `classification`, `max_chars`,
|
|
|
738
798
|
`regex_absent`, `regex_present`, `non_empty`, and `llm_judge` graders; and
|
|
739
799
|
`markdown` + `html` reporters (single scorecards and comparative
|
|
740
800
|
comparisons; pick one with `--report`). Register more with
|
|
741
|
-
`
|
|
742
|
-
`
|
|
801
|
+
`evalcore.adapters.base.register` / `evalcore.graders.base.register` /
|
|
802
|
+
`evalcore.reporters.base.register` and load them with `--plugins your.module`
|
|
743
803
|
(CLI) or a plain import (Python API). If onboarding a new consumer ever
|
|
744
|
-
requires touching `src/
|
|
804
|
+
requires touching `src/evalcore/`, that's an abstraction leak - fix the engine
|
|
745
805
|
seam, don't fork it.
|
|
746
806
|
|
|
747
807
|
## Layout
|
|
748
808
|
|
|
749
809
|
```
|
|
750
|
-
src/
|
|
810
|
+
src/evalcore/
|
|
751
811
|
models.py Case, Variant, Output, Score, Scorecard, Comparison (opaque-blob based)
|
|
752
812
|
refs.py $ref resolution (the only thing that opens a consumer's blobs)
|
|
753
813
|
loader.py suite + dataset loading, content hashes (YAML/JSON; suite-relative paths)
|
|
@@ -795,9 +855,9 @@ Known gaps / next:
|
|
|
795
855
|
## Releasing
|
|
796
856
|
|
|
797
857
|
Releases publish to PyPI as **`evalcore`** via
|
|
798
|
-
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC)
|
|
858
|
+
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC) - no API
|
|
799
859
|
tokens are stored. One-time setup on PyPI: add a *pending publisher* for project
|
|
800
860
|
`evalcore` pointing at owner `scottpmiller`, repo `evalcore`, workflow
|
|
801
861
|
`publish.yml`, environment `pypi`. Then to cut a release: bump `version` in
|
|
802
|
-
`pyproject.toml`, tag it, and publish a GitHub Release
|
|
862
|
+
`pyproject.toml`, tag it, and publish a GitHub Release - `.github/workflows/publish.yml`
|
|
803
863
|
builds the sdist + wheel and uploads them. (Point it at TestPyPI first for a dry run.)
|