evalproof 0.2.0__py3-none-any.whl
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.
- evalproof/__init__.py +69 -0
- evalproof/_version.py +3 -0
- evalproof/cache.py +89 -0
- evalproof/calibration.py +266 -0
- evalproof/cli.py +265 -0
- evalproof/compare.py +169 -0
- evalproof/config.py +158 -0
- evalproof/dataset.py +66 -0
- evalproof/gate.py +107 -0
- evalproof/models.py +115 -0
- evalproof/providers/__init__.py +54 -0
- evalproof/providers/anthropic.py +84 -0
- evalproof/providers/base.py +75 -0
- evalproof/providers/ollama.py +70 -0
- evalproof/providers/openai.py +76 -0
- evalproof/py.typed +0 -0
- evalproof/report.py +149 -0
- evalproof/runner.py +135 -0
- evalproof/scorers/__init__.py +80 -0
- evalproof/scorers/deterministic.py +227 -0
- evalproof/scorers/judge.py +187 -0
- evalproof/scorers/retrieval.py +64 -0
- evalproof/stats.py +145 -0
- evalproof/systems.py +154 -0
- evalproof-0.2.0.dist-info/METADATA +279 -0
- evalproof-0.2.0.dist-info/RECORD +30 -0
- evalproof-0.2.0.dist-info/WHEEL +4 -0
- evalproof-0.2.0.dist-info/entry_points.txt +2 -0
- evalproof-0.2.0.dist-info/licenses/LICENSE +202 -0
- evalproof-0.2.0.dist-info/licenses/NOTICE +4 -0
evalproof/systems.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Systems under test: an LLM prompt, a Python function, or an HTTP endpoint.
|
|
2
|
+
|
|
3
|
+
A system failure is recorded on the case, not raised: one timeout should not throw
|
|
4
|
+
away an hour of judging. Errored cases score as failures and are listed in reports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import string
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any, Protocol
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from evalproof.cache import CachedProvider, ResponseCache
|
|
19
|
+
from evalproof.config import ConfigError, TargetConfig
|
|
20
|
+
from evalproof.models import Case, SystemOutput
|
|
21
|
+
from evalproof.providers import Message, Provider, ProviderError, make_provider, with_retries
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class System(Protocol):
|
|
25
|
+
name: str
|
|
26
|
+
|
|
27
|
+
def run(self, case: Case) -> SystemOutput: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render(template: str, variables: dict[str, Any]) -> str:
|
|
31
|
+
"""``str.format`` with a clear error naming the missing field."""
|
|
32
|
+
missing = [
|
|
33
|
+
field
|
|
34
|
+
for _, field, _, _ in string.Formatter().parse(template)
|
|
35
|
+
if field and field.split(".")[0].split("[")[0] not in variables
|
|
36
|
+
]
|
|
37
|
+
if missing:
|
|
38
|
+
raise ConfigError(f"prompt template references unknown fields: {sorted(set(missing))}")
|
|
39
|
+
return template.format_map(variables)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMSystem:
|
|
43
|
+
def __init__(self, cfg: TargetConfig, provider: Provider) -> None:
|
|
44
|
+
self.cfg = cfg
|
|
45
|
+
self.provider = provider
|
|
46
|
+
self.name = cfg.label or cfg.target
|
|
47
|
+
|
|
48
|
+
def messages(self, case: Case) -> list[Message]:
|
|
49
|
+
msgs: list[Message] = []
|
|
50
|
+
if self.cfg.system_prompt:
|
|
51
|
+
msgs.append(Message(role="system", content=self.cfg.system_prompt))
|
|
52
|
+
msgs.append(
|
|
53
|
+
Message(role="user", content=render(self.cfg.prompt_template, case.render_vars()))
|
|
54
|
+
)
|
|
55
|
+
return msgs
|
|
56
|
+
|
|
57
|
+
def run(self, case: Case) -> SystemOutput:
|
|
58
|
+
try:
|
|
59
|
+
resp = self.provider.chat(self.messages(case), self.cfg.params)
|
|
60
|
+
except ProviderError as exc:
|
|
61
|
+
return SystemOutput(output="", error=str(exc))
|
|
62
|
+
return SystemOutput(
|
|
63
|
+
output=resp.text,
|
|
64
|
+
latency_ms=resp.latency_ms,
|
|
65
|
+
tokens_in=resp.tokens_in,
|
|
66
|
+
tokens_out=resp.tokens_out,
|
|
67
|
+
cost_usd=self.cfg.pricing.cost(resp.tokens_in, resp.tokens_out),
|
|
68
|
+
cached=resp.cached,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _coerce(result: Any) -> tuple[str, list[str]]:
|
|
73
|
+
if isinstance(result, str):
|
|
74
|
+
return result, []
|
|
75
|
+
if isinstance(result, dict) and "output" in result:
|
|
76
|
+
return str(result["output"]), [str(x) for x in result.get("retrieved") or []]
|
|
77
|
+
raise TypeError("a system must return a string or a dict with an 'output' key")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PythonSystem:
|
|
81
|
+
"""``python:package.module:function`` — the function receives the case input.
|
|
82
|
+
|
|
83
|
+
``search_path`` (the suite's directory) is put on ``sys.path`` so a suite can ship
|
|
84
|
+
its system next to it and still run from any working directory.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, spec: str, search_path: str | None = None) -> None:
|
|
88
|
+
self.name = spec
|
|
89
|
+
if search_path and search_path not in sys.path:
|
|
90
|
+
sys.path.insert(0, search_path)
|
|
91
|
+
try:
|
|
92
|
+
_, module_name, func_name = spec.split(":", 2)
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
raise ConfigError(
|
|
95
|
+
f"python target must be 'python:<module>:<function>', got {spec!r}"
|
|
96
|
+
) from exc
|
|
97
|
+
module = importlib.import_module(module_name)
|
|
98
|
+
func = getattr(module, func_name, None)
|
|
99
|
+
if not callable(func):
|
|
100
|
+
raise ConfigError(f"{module_name}.{func_name} is not callable")
|
|
101
|
+
self.func: Callable[[Any], Any] = func
|
|
102
|
+
|
|
103
|
+
def run(self, case: Case) -> SystemOutput:
|
|
104
|
+
start = time.perf_counter()
|
|
105
|
+
try:
|
|
106
|
+
output, retrieved = _coerce(self.func(case.input))
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
return SystemOutput(output="", error=f"{type(exc).__name__}: {exc}")
|
|
109
|
+
return SystemOutput(
|
|
110
|
+
output=output, retrieved=retrieved, latency_ms=(time.perf_counter() - start) * 1000
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class HttpSystem:
|
|
115
|
+
"""POSTs ``{"id", "input"}``; expects JSON ``{"output", "retrieved"?}`` or plain text."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, cfg: TargetConfig, client: httpx.Client | None = None) -> None:
|
|
118
|
+
self.cfg = cfg
|
|
119
|
+
self.name = cfg.label or cfg.target
|
|
120
|
+
self._client = client or httpx.Client(timeout=cfg.timeout_s)
|
|
121
|
+
|
|
122
|
+
def run(self, case: Case) -> SystemOutput:
|
|
123
|
+
def call() -> httpx.Response:
|
|
124
|
+
resp = self._client.post(self.cfg.target, json={"id": case.id, "input": case.input})
|
|
125
|
+
resp.raise_for_status()
|
|
126
|
+
return resp
|
|
127
|
+
|
|
128
|
+
start = time.perf_counter()
|
|
129
|
+
try:
|
|
130
|
+
resp = with_retries(call)
|
|
131
|
+
ctype = resp.headers.get("content-type", "")
|
|
132
|
+
output, retrieved = _coerce(resp.json() if "json" in ctype else resp.text)
|
|
133
|
+
except (ProviderError, TypeError, ValueError) as exc:
|
|
134
|
+
return SystemOutput(output="", error=str(exc))
|
|
135
|
+
return SystemOutput(
|
|
136
|
+
output=output, retrieved=retrieved, latency_ms=(time.perf_counter() - start) * 1000
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def make_system(
|
|
141
|
+
cfg: TargetConfig,
|
|
142
|
+
cache: ResponseCache | None = None,
|
|
143
|
+
client: httpx.Client | None = None,
|
|
144
|
+
search_path: str | None = None,
|
|
145
|
+
) -> System:
|
|
146
|
+
kind = cfg.kind
|
|
147
|
+
if kind == "python":
|
|
148
|
+
return PythonSystem(cfg.target, search_path=search_path)
|
|
149
|
+
if kind == "http":
|
|
150
|
+
return HttpSystem(cfg, client=client)
|
|
151
|
+
provider: Provider = make_provider(cfg, client=client)
|
|
152
|
+
if cache is not None:
|
|
153
|
+
provider = CachedProvider(provider, cache, base_url=cfg.base_url)
|
|
154
|
+
return LLMSystem(cfg, provider)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evalproof
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Treat LLM evaluation as measurement: calibrate the judge, correct the pass rate, gate CI on significant regressions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/technobasant/evalproof
|
|
6
|
+
Project-URL: Documentation, https://github.com/technobasant/evalproof#readme
|
|
7
|
+
Project-URL: Source, https://github.com/technobasant/evalproof
|
|
8
|
+
Project-URL: Issues, https://github.com/technobasant/evalproof/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/technobasant/evalproof/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Live demo, https://evalproof.streamlit.app
|
|
11
|
+
Project-URL: Write-up, https://basantbhattarai.com.np/writing/eval-score-drop-twelve-answers/
|
|
12
|
+
Author: Basant Bhattarai
|
|
13
|
+
License-Expression: Apache-2.0
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
License-File: NOTICE
|
|
16
|
+
Keywords: calibration,ci,evaluation,llm,llm-as-a-judge,statistics
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Classifier: Environment :: Console
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Intended Audience :: Science/Research
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
29
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
30
|
+
Classifier: Topic :: Software Development :: Testing
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.11
|
|
33
|
+
Requires-Dist: httpx>=0.27
|
|
34
|
+
Requires-Dist: numpy>=1.26
|
|
35
|
+
Requires-Dist: pydantic>=2.7
|
|
36
|
+
Requires-Dist: pyyaml>=6.0
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# evalproof
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/evalproof/)
|
|
42
|
+
[](https://pypi.org/project/evalproof/)
|
|
43
|
+
[](https://github.com/technobasant/evalproof/actions/workflows/ci.yml)
|
|
44
|
+
[](https://github.com/technobasant/evalproof/blob/main/LICENSE)
|
|
45
|
+
[](https://evalproof.streamlit.app)
|
|
46
|
+
|
|
47
|
+
**Treat LLM evaluation as measurement.** Calibrate the judge, correct its pass rate, and
|
|
48
|
+
gate CI on regressions that are real rather than noise.
|
|
49
|
+
|
|
50
|
+
**Try it in the browser: [evalproof.streamlit.app](https://evalproof.streamlit.app).** Check whether
|
|
51
|
+
a drop between two runs is real, or replay the run below and read the twelve answers that
|
|
52
|
+
changed. It runs this repository's code; after a quiet spell it takes about half a minute to wake.
|
|
53
|
+
|
|
54
|
+
An LLM judge is a classifier with error rates. If it passes bad answers at rate FPR and
|
|
55
|
+
good answers at rate TPR, its pass rate converges to `FPR + (TPR − FPR) · p`, not to the
|
|
56
|
+
true rate `p`. That is classify-and-count, and it is biased the way any classifier's count
|
|
57
|
+
is — the effect is worked through with a seeded simulation in
|
|
58
|
+
[*The classifier was 95% accurate. The trend it counted was a third too flat*](https://basantbhattarai.com.np/writing/count-prevalence-not-classifications/)
|
|
59
|
+
([lab](https://github.com/technobasant/prevalence-shift)).
|
|
60
|
+
|
|
61
|
+
evalproof:
|
|
62
|
+
|
|
63
|
+
1. **measures the judge** against a small gold-labelled sample — confusion matrix, TPR,
|
|
64
|
+
FPR, Cohen's κ;
|
|
65
|
+
2. **corrects its pass rate** with adjusted classify-and-count and prediction-powered
|
|
66
|
+
inference, each with a confidence interval;
|
|
67
|
+
3. **compares runs on paired cases** — exact McNemar for pass/fail, paired bootstrap for
|
|
68
|
+
scores;
|
|
69
|
+
4. **gates CI** only when a drop is both larger than your tolerance *and* statistically
|
|
70
|
+
significant, plus latency and cost budgets, with distinct exit codes.
|
|
71
|
+
|
|
72
|
+
It is local-first: Ollama is the default provider, Anthropic and OpenAI are optional, and
|
|
73
|
+
every model call is cached by content so re-runs are free and reproducible.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install evalproof # or: uv add evalproof
|
|
79
|
+
uv tool install evalproof # just the CLI
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Python 3.11 to 3.14. `compare`, `calibrate` and `report` work on run files and need no
|
|
83
|
+
model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local
|
|
84
|
+
Ollama model, or an Anthropic or OpenAI key.
|
|
85
|
+
|
|
86
|
+
## What it found on a real run
|
|
87
|
+
|
|
88
|
+
A local `qwen3:1.7b` support assistant for a fictional product, judged by `qwen3:8b`, 83
|
|
89
|
+
questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md).
|
|
90
|
+
|
|
91
|
+
| Question | Measured |
|
|
92
|
+
|---|---|
|
|
93
|
+
| How good is the 8B judge? | TPR 92.6%, FPR 6.7%, Cohen's κ 0.78 against the answer key |
|
|
94
|
+
| Did swapping the grounded prompt for a "friendly" one regress quality? | −4.8 pts on the judge, 8 cases worse vs 4 better, McNemar p = 0.39 — **gate warns, does not fail**; a delta-only gate would have failed it |
|
|
95
|
+
| What did regress? | p95 latency, 1,469 → 4,422 ms; mean answer 19 → 77 tokens. A 2,500 ms budget fails the change with exit 3 |
|
|
96
|
+
| Does correcting the judge's rate help? | On average yes (offset 4.8 → ~3.8 pts), but with 30 labels the interval doubles; at 83 cases the judge's bias is smaller than its own noise |
|
|
97
|
+
| Was the gold trustworthy? | No: of 13 judge–key disagreements the key was wrong 7 times, the judge 4. Most of the "bias" corrected was the gold's |
|
|
98
|
+
| Is pairwise judging safer? | No: 42% of verdicts flipped with the order of the two answers, and the judge preferred the candidate 42 to 6 — mostly where both answers were right and the candidate's was five times longer |
|
|
99
|
+
| Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s |
|
|
100
|
+
|
|
101
|
+
## Five-minute quickstart
|
|
102
|
+
|
|
103
|
+
The offline example lives in the repository:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/technobasant/evalproof && cd evalproof
|
|
107
|
+
uv sync
|
|
108
|
+
|
|
109
|
+
# Offline: no model, no network. Four versions of a ticket router.
|
|
110
|
+
for v in baseline regressed noisy refactored; do
|
|
111
|
+
uv run evalproof run examples/deterministic/suite.yaml \
|
|
112
|
+
--system "python:router:${v}_route" --out "runs/$v.json" --quiet
|
|
113
|
+
done
|
|
114
|
+
uv run evalproof compare runs/baseline.json runs/regressed.json \
|
|
115
|
+
--suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 1: real regression
|
|
116
|
+
uv run evalproof compare runs/baseline.json runs/noisy.json \
|
|
117
|
+
--suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 0: 2 of 60 is noise
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
With Ollama installed, the LLM example is
|
|
121
|
+
[`examples/support-qa`](https://github.com/technobasant/evalproof/blob/main/examples/support-qa) (`ollama pull qwen3:1.7b` and
|
|
122
|
+
`ollama pull qwen3:8b`; about 6–10 minutes per run on an Apple M5).
|
|
123
|
+
|
|
124
|
+
The browser app in [`app/`](https://github.com/technobasant/evalproof/blob/main/app) runs locally too:
|
|
125
|
+
`uv run --with-requirements app/requirements.txt streamlit run app/streamlit_app.py`.
|
|
126
|
+
|
|
127
|
+
## Concepts
|
|
128
|
+
|
|
129
|
+
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
130
|
+
judge, calibration settings and a gate.
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
name: support-qa
|
|
134
|
+
dataset: cases.jsonl # {"id", "input", "reference", ...extra fields}
|
|
135
|
+
system:
|
|
136
|
+
target: ollama:qwen3:1.7b # or anthropic:…, openai:…, python:module:func, https://…
|
|
137
|
+
system_prompt_file: prompt.md
|
|
138
|
+
prompt_template: "{question}"
|
|
139
|
+
params: {temperature: 0, seed: 0, think: false}
|
|
140
|
+
judge:
|
|
141
|
+
target: ollama:qwen3:8b
|
|
142
|
+
scorers:
|
|
143
|
+
- {name: answer_key, type: regex, params: {any_of_field: accept, none_of_field: reject}}
|
|
144
|
+
- {name: judge, type: llm_judge, params: {rubric_file: rubric.md}}
|
|
145
|
+
calibration: {judge_scorer: judge, gold_scorer: answer_key, gold_sample: 30}
|
|
146
|
+
gate: {metric: judge, max_drop: 0.03, alpha: 0.05, min_cases: 40, max_p95_latency_ms: 2500}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Run** — `evalproof run suite.yaml` writes one self-describing JSON file: every case's
|
|
150
|
+
output, latency, tokens and scores, plus the dataset hash, seed, Ollama version and model
|
|
151
|
+
digests.
|
|
152
|
+
|
|
153
|
+
**Scorers** — `exact_match`, `contains`, `regex` (accept/reject answer keys),
|
|
154
|
+
`json_schema`, `numeric_tolerance`, `recall_at_k`, `mrr`, `ndcg`, and `llm_judge`: a strict
|
|
155
|
+
JSON verdict, one retry on an unparseable reply, and an explicit *undecided* state that is
|
|
156
|
+
counted, never scored as a pass. `evalproof pairwise` judges two runs head to head in both
|
|
157
|
+
orders and reports order-dependent verdicts as ties.
|
|
158
|
+
|
|
159
|
+
**Calibrate** — `evalproof calibrate run.json --judge-scorer judge --labels gold.jsonl`
|
|
160
|
+
(human labels) or `--gold-scorer answer_key --gold-sample 30`:
|
|
161
|
+
|
|
162
|
+
| Estimate | What it is |
|
|
163
|
+
|---|---|
|
|
164
|
+
| raw judge pass rate | classify-and-count, Wilson interval |
|
|
165
|
+
| gold labels only | the labelled sample alone |
|
|
166
|
+
| ACC | `(q − FPR) / (TPR − FPR)`, bootstrap interval over both the judged set and the gold sample |
|
|
167
|
+
| PPI | judge mean on unlabelled cases + mean (label − judge) on the gold sample, normal interval |
|
|
168
|
+
|
|
169
|
+
**Compare and gate** — `evalproof compare baseline.json candidate.json --suite suite.yaml --gate`
|
|
170
|
+
|
|
171
|
+
| Exit | Meaning |
|
|
172
|
+
|---|---|
|
|
173
|
+
| 0 | pass — possibly with warnings (a big drop that is not significant; a significant drop within tolerance) |
|
|
174
|
+
| 1 | quality regression: drop ≥ `max_drop` **and** p < `alpha` |
|
|
175
|
+
| 3 | budget breach: candidate p95 latency or cost over the limit |
|
|
176
|
+
| 4 | cannot gate: fewer than `min_cases` paired cases, or the runs used different data |
|
|
177
|
+
| 2 | usage or configuration error |
|
|
178
|
+
|
|
179
|
+
## From Python
|
|
180
|
+
|
|
181
|
+
The CLI is a thin layer over the same functions:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from evalproof import GateConfig, calibrate, compare_runs, decide, load_run
|
|
185
|
+
|
|
186
|
+
baseline = load_run("runs/baseline.json")
|
|
187
|
+
candidate = load_run("runs/candidate.json")
|
|
188
|
+
|
|
189
|
+
cmp = compare_runs(baseline, candidate) # paired cases: exact McNemar, bootstrap intervals
|
|
190
|
+
decision = decide(cmp, GateConfig(metric="judge", max_drop=0.03))
|
|
191
|
+
print(decision.status, decision.exit_code, decision.warnings)
|
|
192
|
+
|
|
193
|
+
report = calibrate(baseline, "judge", gold_scorer="answer_key", gold_sample=30)
|
|
194
|
+
for est in report.estimates: # raw judge rate, gold only, ACC, PPI
|
|
195
|
+
print(est.method, est.point, est.low, est.high)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The names in `evalproof.__all__` are the supported API. Other modules are internal and may
|
|
199
|
+
change between minor versions; see the [changelog](https://github.com/technobasant/evalproof/blob/main/CHANGELOG.md).
|
|
200
|
+
|
|
201
|
+
## Architecture
|
|
202
|
+
|
|
203
|
+
The architecture diagram is in the [README on GitHub](https://github.com/technobasant/evalproof#architecture).
|
|
204
|
+
|
|
205
|
+
## Decisions
|
|
206
|
+
|
|
207
|
+
| Decision | Alternative | Why | ADR |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| Treat the judge as a classifier to be calibrated | Trust the judge's pass rate | Its count is biased by its own error rates, and the bias moves with the true rate | [0001](https://github.com/technobasant/evalproof/blob/main/docs/adr/0001-judge-is-a-classifier.md) |
|
|
210
|
+
| Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](https://github.com/technobasant/evalproof/blob/main/docs/adr/0002-local-first-providers.md) |
|
|
211
|
+
| Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](https://github.com/technobasant/evalproof/blob/main/docs/adr/0003-gate-on-significance-not-delta.md) |
|
|
212
|
+
| Gate one metric; report the rest | Gate every metric | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
213
|
+
| Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md) |
|
|
214
|
+
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
215
|
+
|
|
216
|
+
## In CI
|
|
217
|
+
|
|
218
|
+
The composite action runs `compare --gate`, appends the report to the job summary, and
|
|
219
|
+
fails the job on exit codes 1, 3 or 4:
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
- uses: technobasant/evalproof@v0.2.0
|
|
223
|
+
with:
|
|
224
|
+
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
225
|
+
candidate: eval/candidate.json # produced on this branch
|
|
226
|
+
suite: eval/suite.yaml
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This repository's own CI runs the offline example on every commit and asserts that the
|
|
230
|
+
gate returns 1 for the real regression and 0 for the noisy and refactored candidates.
|
|
231
|
+
|
|
232
|
+
## How it relates to other tools
|
|
233
|
+
|
|
234
|
+
[promptfoo](https://www.promptfoo.dev/docs/intro/) (open-source, local evals of prompts,
|
|
235
|
+
models and RAG pipelines with deterministic and model-graded assertions, a GitHub Action,
|
|
236
|
+
and red teaming), [DeepEval](https://deepeval.com/docs/getting-started) (pytest-style
|
|
237
|
+
LLM tests with research-backed metrics such as G-Eval), [Ragas](https://docs.ragas.io/en/stable/)
|
|
238
|
+
(LLM-driven metrics and experiments for LLM and RAG applications) and
|
|
239
|
+
[Inspect](https://inspect.aisi.org.uk/) (the UK AI Security Institute's framework for
|
|
240
|
+
model and agent evaluations, with sandboxed tools and a log viewer) are broad evaluation
|
|
241
|
+
frameworks.
|
|
242
|
+
|
|
243
|
+
evalproof is deliberately narrow: it is the measurement layer — how much a judge's verdict
|
|
244
|
+
is worth, and whether a difference between two runs is real. Its run file is plain,
|
|
245
|
+
documented JSON, so it can sit beside an existing harness: produce the per-case verdicts
|
|
246
|
+
however you like, then calibrate and gate here.
|
|
247
|
+
|
|
248
|
+
## Limits
|
|
249
|
+
|
|
250
|
+
* The measurements cover one machine, two small local models, one fictional domain and
|
|
251
|
+
83 cases. They show the method; they are not a benchmark of those models.
|
|
252
|
+
* Calibration is only as good as its gold labels — [MEASUREMENTS §4](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md)
|
|
253
|
+
shows a regex answer key being wrong more often than the judge. Use human labels when
|
|
254
|
+
the decision matters.
|
|
255
|
+
* ACC needs both classes in the gold sample and a judge better than chance; PPI needs the
|
|
256
|
+
gold sample to be random. evalproof reports when either does not hold rather than
|
|
257
|
+
printing a number.
|
|
258
|
+
* The percentile-bootstrap p-value for continuous scores is an approximation; pass/fail
|
|
259
|
+
metrics use the exact McNemar test.
|
|
260
|
+
* Cost tracking needs a price table in the suite for hosted models.
|
|
261
|
+
|
|
262
|
+
## Roadmap
|
|
263
|
+
|
|
264
|
+
* Stratified gold sampling (by tag) and per-tag calibration.
|
|
265
|
+
* Multiple-comparison correction when more than one metric is gated.
|
|
266
|
+
* A reasoning judge (`think: true`) measured against the same answer key.
|
|
267
|
+
* Importers for other harnesses' per-case results.
|
|
268
|
+
|
|
269
|
+
## Project
|
|
270
|
+
|
|
271
|
+
[Changelog](https://github.com/technobasant/evalproof/blob/main/CHANGELOG.md) · [Contributing](https://github.com/technobasant/evalproof/blob/main/CONTRIBUTING.md) ·
|
|
272
|
+
[Security policy](https://github.com/technobasant/evalproof/blob/main/SECURITY.md) · [Code of conduct](https://github.com/technobasant/evalproof/blob/main/CODE_OF_CONDUCT.md) ·
|
|
273
|
+
[How to cite](https://github.com/technobasant/evalproof/blob/main/CITATION.cff). Releases are published to PyPI from GitHub Actions with
|
|
274
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/), and each file carries a
|
|
275
|
+
signed provenance attestation.
|
|
276
|
+
|
|
277
|
+
## Licence
|
|
278
|
+
|
|
279
|
+
Apache-2.0. See [LICENSE](https://github.com/technobasant/evalproof/blob/main/LICENSE) and [NOTICE](https://github.com/technobasant/evalproof/blob/main/NOTICE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
evalproof/__init__.py,sha256=EXJgFmkOEU6CCP8mhb1k1B-r9Lt2v1zttIxP13LuiiE,1953
|
|
2
|
+
evalproof/_version.py,sha256=8LOhs9njNsWQUbKWbELE0YghKdN7GOQChQgEnFWM4Ps,121
|
|
3
|
+
evalproof/cache.py,sha256=hzJB7jV6hCzT6F_GZDB8NpVXh4dgiwtc-8Ni56WPAs8,3066
|
|
4
|
+
evalproof/calibration.py,sha256=oRqg7qSnKKd2gwk7boNSYwsarYDLRlNKWb5Mwe3E2TY,9552
|
|
5
|
+
evalproof/cli.py,sha256=qlLgxkhZysXOgVzP9FxUWzgoyTYRwu1rD7xiXKFiD00,9831
|
|
6
|
+
evalproof/compare.py,sha256=iyNddsgjPLYJ1IC3KJyxs989tsYheukoDPMnsFD2wEI,5297
|
|
7
|
+
evalproof/config.py,sha256=1R0Dxgegx_knJSGbFylfBi3pQK-aUPw0_S6E5-A8SEA,5329
|
|
8
|
+
evalproof/dataset.py,sha256=5bvBaPajQxNnaUFAWwgEL6OnRRsQiT0WsYI7FPW3w1E,2346
|
|
9
|
+
evalproof/gate.py,sha256=R-cm0TQaQWcnY8LkJOJy4hQNT2imK1MNV0_QnC51jt4,4036
|
|
10
|
+
evalproof/models.py,sha256=g3AyW9rJRKuKrE1dNrZWvwADqhY3xxl4VEv01XCfR7k,3332
|
|
11
|
+
evalproof/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
evalproof/report.py,sha256=q2fVojAQTlKiBfgxSbTl7uBEvsJkjvvaxO1zxj83IfA,5831
|
|
13
|
+
evalproof/runner.py,sha256=6QCvComHEa02Ql28qnQiOqm0SUzsu8f7qn1caOH3Bt8,4724
|
|
14
|
+
evalproof/stats.py,sha256=Xwt5k7g29uC7_l2rs-Vyw14XKYBr9Mn_APXy75ewXDE,4992
|
|
15
|
+
evalproof/systems.py,sha256=pvAV02QVRnPsVZxRMivJDW-mlhPtAt1qcLCS3jnjNEI,5631
|
|
16
|
+
evalproof/providers/__init__.py,sha256=YIptAL2kK6LU7rcuyUV2LrCv9KjpDjYwXCLz2aUzdis,1785
|
|
17
|
+
evalproof/providers/anthropic.py,sha256=jogTjgxQCad3D-E-DAyFnfzgODB2pmago2Ujy3rCGpw,2623
|
|
18
|
+
evalproof/providers/base.py,sha256=XNRiu5S1vyfwU1EzmaAPmL6v-vsHvOBteJ3jnRqv1Lk,2136
|
|
19
|
+
evalproof/providers/ollama.py,sha256=MklL_Ex4vGeDc-fyV3NxscZwk7cRFgKDr3Zx7Tpd1Dk,2306
|
|
20
|
+
evalproof/providers/openai.py,sha256=2mviallmw4BJOblE596z0_djOgVxvo-4MpZGwn7e3kA,2626
|
|
21
|
+
evalproof/scorers/__init__.py,sha256=s-aCK5o39XASypzT-Z2cE3H61Asg3g9mqHiktyzFCSw,2205
|
|
22
|
+
evalproof/scorers/deterministic.py,sha256=4u7wfl9WG4EQKbAErDCrSwTyzfCzd11ck8JS3btBzRk,8393
|
|
23
|
+
evalproof/scorers/judge.py,sha256=crRCZI7XFOV-U_3Wxo0XZVYlsA-In1mCm_FnXJwxc4c,6365
|
|
24
|
+
evalproof/scorers/retrieval.py,sha256=QAEKPPfdZWpR23g9ieNzsIIxv6Vt4QTY-pkC4JIyhYc,2291
|
|
25
|
+
evalproof-0.2.0.dist-info/METADATA,sha256=eCcYpx36acVv13QinExbSu_w0Yg9v6cBgZkGz9C7_nE,15308
|
|
26
|
+
evalproof-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
27
|
+
evalproof-0.2.0.dist-info/entry_points.txt,sha256=q81031vTHHlprNogYBcd0RNWW9UgchMn1kHCMDzZ_24,49
|
|
28
|
+
evalproof-0.2.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
29
|
+
evalproof-0.2.0.dist-info/licenses/NOTICE,sha256=QKLv_qIUn1TOjv_xERUUCCiWJBxtuUJxTV--1WVCLjw,121
|
|
30
|
+
evalproof-0.2.0.dist-info/RECORD,,
|