proofstep-core 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.
- proofstep_core-0.1.0/.gitignore +57 -0
- proofstep_core-0.1.0/PKG-INFO +39 -0
- proofstep_core-0.1.0/README.md +17 -0
- proofstep_core-0.1.0/pyproject.toml +38 -0
- proofstep_core-0.1.0/src/proofstep_core/__init__.py +59 -0
- proofstep_core-0.1.0/src/proofstep_core/aggregate.py +140 -0
- proofstep_core-0.1.0/src/proofstep_core/calibration.py +849 -0
- proofstep_core-0.1.0/src/proofstep_core/calibration_runner.py +471 -0
- proofstep_core-0.1.0/src/proofstep_core/compare.py +205 -0
- proofstep_core-0.1.0/src/proofstep_core/dataset.py +174 -0
- proofstep_core-0.1.0/src/proofstep_core/evaluators/__init__.py +47 -0
- proofstep_core-0.1.0/src/proofstep_core/evaluators/deterministic.py +404 -0
- proofstep_core-0.1.0/src/proofstep_core/evaluators/judge.py +335 -0
- proofstep_core-0.1.0/src/proofstep_core/evaluators/operational.py +127 -0
- proofstep_core-0.1.0/src/proofstep_core/evaluators/statistical.py +434 -0
- proofstep_core-0.1.0/src/proofstep_core/gates.py +545 -0
- proofstep_core-0.1.0/src/proofstep_core/paths.py +98 -0
- proofstep_core-0.1.0/src/proofstep_core/py.typed +0 -0
- proofstep_core-0.1.0/src/proofstep_core/redaction.py +263 -0
- proofstep_core-0.1.0/src/proofstep_core/runner.py +482 -0
- proofstep_core-0.1.0/src/proofstep_core/sampling.py +200 -0
- proofstep_core-0.1.0/src/proofstep_core/significance.py +477 -0
- proofstep_core-0.1.0/src/proofstep_core/stats.py +142 -0
- proofstep_core-0.1.0/src/proofstep_core/suite.py +277 -0
- proofstep_core-0.1.0/src/proofstep_core/testing.py +118 -0
- proofstep_core-0.1.0/src/proofstep_core/types.py +156 -0
- proofstep_core-0.1.0/src/proofstep_core/versioning.py +63 -0
- proofstep_core-0.1.0/tests/test_aggregate.py +103 -0
- proofstep_core-0.1.0/tests/test_calibration.py +622 -0
- proofstep_core-0.1.0/tests/test_compare.py +147 -0
- proofstep_core-0.1.0/tests/test_gates.py +349 -0
- proofstep_core-0.1.0/tests/test_gates_significance.py +201 -0
- proofstep_core-0.1.0/tests/test_hidden_regression.py +156 -0
- proofstep_core-0.1.0/tests/test_judge.py +195 -0
- proofstep_core-0.1.0/tests/test_runner.py +260 -0
- proofstep_core-0.1.0/tests/test_sampling.py +230 -0
- proofstep_core-0.1.0/tests/test_significance.py +337 -0
- proofstep_core-0.1.0/tests/test_statistical.py +181 -0
- proofstep_core-0.1.0/tests/test_stats.py +123 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
__pycache__/
|
|
3
|
+
.coverage
|
|
4
|
+
.coverage.*
|
|
5
|
+
.docker-data/
|
|
6
|
+
.DS_Store
|
|
7
|
+
.e2e-api.log
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
.env.prod
|
|
11
|
+
# Exceptions, and they must come *after* the patterns above: git takes the last matching rule, so a
|
|
12
|
+
# negation written earlier in the file is silently overridden. That is not a hypothetical — the `!`
|
|
13
|
+
# line used to sit at the top, `.env.*` re-ignored it, and `.env.prod.example` was never committed.
|
|
14
|
+
# `scripts/init_secrets.sh` reads that file, so the first documented step of self-hosting failed for
|
|
15
|
+
# anyone who cloned the repository. It was caught by CI running the same step.
|
|
16
|
+
!.env.example
|
|
17
|
+
!.env.prod.example
|
|
18
|
+
.hypothesis/
|
|
19
|
+
.idea/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.next/
|
|
22
|
+
.proofstep/
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.turbo/
|
|
26
|
+
.venv/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
*.key
|
|
30
|
+
*.pem
|
|
31
|
+
*.proofstep.local.yaml
|
|
32
|
+
*.py[cod]
|
|
33
|
+
*.swp
|
|
34
|
+
*.tsbuildinfo
|
|
35
|
+
# Database dumps. Never committed: they contain every tenant's data, and a backup in a git history
|
|
36
|
+
# Docker volumes
|
|
37
|
+
# Editors / OS
|
|
38
|
+
# is a backup with no access control and no retention.
|
|
39
|
+
# Node
|
|
40
|
+
# Proofstep local state
|
|
41
|
+
# Python
|
|
42
|
+
# Reports a local run drops in the working tree. The name comes from the suite, so the pattern has
|
|
43
|
+
# Secrets and local config — never commit these
|
|
44
|
+
# The e2e stack's server log, written next to the repo so a CI failure can print it.
|
|
45
|
+
# to cover all of them rather than the default filename only.
|
|
46
|
+
~/.proofstep/
|
|
47
|
+
backups/
|
|
48
|
+
build/
|
|
49
|
+
coverage.xml
|
|
50
|
+
credentials.json
|
|
51
|
+
dist/
|
|
52
|
+
htmlcov/
|
|
53
|
+
node_modules/
|
|
54
|
+
out/
|
|
55
|
+
proofstep-*.json
|
|
56
|
+
secrets/
|
|
57
|
+
venv/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: proofstep-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Proofstep evaluation engine — datasets, evaluators, runner, aggregation, gates
|
|
5
|
+
Project-URL: Homepage, https://github.com/IlaKhan17/proofstep
|
|
6
|
+
Project-URL: Documentation, https://github.com/IlaKhan17/proofstep/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/IlaKhan17/proofstep
|
|
8
|
+
Project-URL: Issues, https://github.com/IlaKhan17/proofstep/issues
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: proofstep-types
|
|
20
|
+
Requires-Dist: pydantic>=2.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# proofstep-core
|
|
24
|
+
|
|
25
|
+
**Evaluators, aggregation, gates, statistics** — part of [Proofstep](https://github.com/IlaKhan17/proofstep), the CI gate for AI
|
|
26
|
+
agents that knows the difference between a regression and a bad day.
|
|
27
|
+
|
|
28
|
+
The pure evaluation library: deterministic and statistical evaluators, LLM-judge harnesses with
|
|
29
|
+
calibration, score aggregation, quality gates, and paired significance testing.
|
|
30
|
+
|
|
31
|
+
No HTTP, no database, no provider SDKs — enforced in CI by an import-linter contract. That boundary
|
|
32
|
+
is what makes local mode, CI mode, and server mode the same code path, and it is why the CLI's exit
|
|
33
|
+
code and a dashboard's verdict cannot drift apart.
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Full documentation lives in the [repository](https://github.com/IlaKhan17/proofstep/tree/main/docs).
|
|
38
|
+
|
|
39
|
+
Apache-2.0.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# proofstep-core
|
|
2
|
+
|
|
3
|
+
**Evaluators, aggregation, gates, statistics** — part of [Proofstep](https://github.com/IlaKhan17/proofstep), the CI gate for AI
|
|
4
|
+
agents that knows the difference between a regression and a bad day.
|
|
5
|
+
|
|
6
|
+
The pure evaluation library: deterministic and statistical evaluators, LLM-judge harnesses with
|
|
7
|
+
calibration, score aggregation, quality gates, and paired significance testing.
|
|
8
|
+
|
|
9
|
+
No HTTP, no database, no provider SDKs — enforced in CI by an import-linter contract. That boundary
|
|
10
|
+
is what makes local mode, CI mode, and server mode the same code path, and it is why the CLI's exit
|
|
11
|
+
code and a dashboard's verdict cannot drift apart.
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
Full documentation lives in the [repository](https://github.com/IlaKhan17/proofstep/tree/main/docs).
|
|
16
|
+
|
|
17
|
+
Apache-2.0.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "proofstep-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Proofstep evaluation engine — datasets, evaluators, runner, aggregation, gates"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 4 - Beta",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"License :: OSI Approved :: Apache Software License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Topic :: Software Development :: Testing",
|
|
15
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
dependencies = [
|
|
20
|
+
"proofstep-types",
|
|
21
|
+
"pydantic>=2.9",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/IlaKhan17/proofstep"
|
|
26
|
+
Documentation = "https://github.com/IlaKhan17/proofstep/tree/main/docs"
|
|
27
|
+
Repository = "https://github.com/IlaKhan17/proofstep"
|
|
28
|
+
Issues = "https://github.com/IlaKhan17/proofstep/issues"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/proofstep_core"]
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
proofstep-types = { workspace = true }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Proofstep evaluation engine (pure library — no I/O).
|
|
2
|
+
|
|
3
|
+
No HTTP, no database, no provider SDKs. Model access arrives through an injected
|
|
4
|
+
`ModelClient` protocol. That boundary is what makes local mode, CI mode, and server
|
|
5
|
+
mode the same code path, and it is enforced in CI by `.importlinter`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib import metadata as _metadata
|
|
9
|
+
|
|
10
|
+
from proofstep_core.aggregate import aggregate_scores, scores_for
|
|
11
|
+
from proofstep_core.compare import Comparison, ExampleRegression, compare_metrics
|
|
12
|
+
from proofstep_core.dataset import Dataset
|
|
13
|
+
from proofstep_core.gates import GateReport, evaluate_gates
|
|
14
|
+
from proofstep_core.paths import PathError, resolve, resolve_in_context
|
|
15
|
+
from proofstep_core.runner import EvalResult, RunConfig, run_suite
|
|
16
|
+
from proofstep_core.suite import EvalSuite, FunctionEvaluator, evaluate
|
|
17
|
+
from proofstep_core.types import (
|
|
18
|
+
CorpusEvaluator,
|
|
19
|
+
EvalContext,
|
|
20
|
+
Evaluator,
|
|
21
|
+
EvaluatorBase,
|
|
22
|
+
Message,
|
|
23
|
+
ModelClient,
|
|
24
|
+
ModelResponse,
|
|
25
|
+
Task,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Read from the installed distribution rather than written here twice. A hand-maintained
|
|
29
|
+
# copy drifts the first time a release bumps one and not the other — which it already did,
|
|
30
|
+
# reporting 0.1.0.dev0 from a 0.1.0 wheel.
|
|
31
|
+
__version__ = _metadata.version("proofstep-core")
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Comparison",
|
|
35
|
+
"CorpusEvaluator",
|
|
36
|
+
"Dataset",
|
|
37
|
+
"EvalContext",
|
|
38
|
+
"EvalResult",
|
|
39
|
+
"EvalSuite",
|
|
40
|
+
"Evaluator",
|
|
41
|
+
"EvaluatorBase",
|
|
42
|
+
"ExampleRegression",
|
|
43
|
+
"FunctionEvaluator",
|
|
44
|
+
"GateReport",
|
|
45
|
+
"Message",
|
|
46
|
+
"ModelClient",
|
|
47
|
+
"ModelResponse",
|
|
48
|
+
"PathError",
|
|
49
|
+
"RunConfig",
|
|
50
|
+
"Task",
|
|
51
|
+
"aggregate_scores",
|
|
52
|
+
"compare_metrics",
|
|
53
|
+
"evaluate",
|
|
54
|
+
"evaluate_gates",
|
|
55
|
+
"resolve",
|
|
56
|
+
"resolve_in_context",
|
|
57
|
+
"run_suite",
|
|
58
|
+
"scores_for",
|
|
59
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Roll per-example scores up into metrics.
|
|
2
|
+
|
|
3
|
+
The load-bearing rule here: **errored evaluations are excluded from the mean and
|
|
4
|
+
counted separately.** A judge that timed out is not a failing example, and averaging
|
|
5
|
+
infrastructure failures in as zeros is the fastest way to make a metric untrustworthy
|
|
6
|
+
(docs/EVALUATION_ENGINE.md §1).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import Iterable, Sequence
|
|
13
|
+
|
|
14
|
+
from proofstep_core.stats import bootstrap_ci, mean, stddev
|
|
15
|
+
from proofstep_types import ExampleResult, Metric, Score
|
|
16
|
+
|
|
17
|
+
# A slice is identified by its sorted key/value pairs so it can be a dict key.
|
|
18
|
+
SliceKey = tuple[tuple[str, str], ...]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _slice_key(slice_: dict[str, str] | None) -> SliceKey:
|
|
22
|
+
return tuple(sorted(slice_.items())) if slice_ else ()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _unkey(key: SliceKey) -> dict[str, str] | None:
|
|
26
|
+
return dict(key) if key else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def aggregate_scores(
|
|
30
|
+
results: Iterable[ExampleResult],
|
|
31
|
+
*,
|
|
32
|
+
slice_by: Sequence[str] = (),
|
|
33
|
+
confidence_intervals: bool = True,
|
|
34
|
+
seed: int = 42,
|
|
35
|
+
) -> list[Metric]:
|
|
36
|
+
"""Aggregate every score across every result into metrics.
|
|
37
|
+
|
|
38
|
+
``slice_by`` names metadata keys to additionally break each metric down by. This
|
|
39
|
+
is what surfaces a rare-class collapse that the aggregate hides: slicing
|
|
40
|
+
``per_class_recall`` by ``class`` makes the unsubscribe number *visible* even
|
|
41
|
+
when nobody thought to gate on it.
|
|
42
|
+
"""
|
|
43
|
+
buckets: dict[tuple[str, SliceKey], list[float]] = defaultdict(list)
|
|
44
|
+
errors: dict[tuple[str, SliceKey], int] = defaultdict(int)
|
|
45
|
+
|
|
46
|
+
for result in results:
|
|
47
|
+
extra = _extra_slices(result, slice_by)
|
|
48
|
+
for score in result.scores:
|
|
49
|
+
own = _slice_key(score.slice)
|
|
50
|
+
for slice_key in {own, *[_merge(own, s) for s in extra]}:
|
|
51
|
+
bucket = (score.metric, slice_key)
|
|
52
|
+
if score.errored:
|
|
53
|
+
errors[bucket] += 1
|
|
54
|
+
elif score.value is not None:
|
|
55
|
+
buckets[bucket].append(score.value)
|
|
56
|
+
|
|
57
|
+
# A metric that produced nothing but errors must still appear, with count 0 —
|
|
58
|
+
# otherwise a gate on it sees "metric missing" and cannot distinguish a typo
|
|
59
|
+
# from a wholly broken evaluator.
|
|
60
|
+
metrics: list[Metric] = []
|
|
61
|
+
for bucket in sorted(set(buckets) | set(errors)):
|
|
62
|
+
metric_key, slice_key = bucket
|
|
63
|
+
values = buckets.get(bucket, [])
|
|
64
|
+
error_count = errors.get(bucket, 0)
|
|
65
|
+
metrics.append(
|
|
66
|
+
_build_metric(
|
|
67
|
+
metric_key,
|
|
68
|
+
values,
|
|
69
|
+
error_count,
|
|
70
|
+
_unkey(slice_key),
|
|
71
|
+
confidence_intervals=confidence_intervals,
|
|
72
|
+
seed=seed,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return metrics
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_metric(
|
|
79
|
+
key: str,
|
|
80
|
+
values: list[float],
|
|
81
|
+
error_count: int,
|
|
82
|
+
slice_: dict[str, str] | None,
|
|
83
|
+
*,
|
|
84
|
+
confidence_intervals: bool,
|
|
85
|
+
seed: int,
|
|
86
|
+
) -> Metric:
|
|
87
|
+
if not values:
|
|
88
|
+
return Metric(key=key, value=0.0, count=0, error_count=error_count, slice=slice_)
|
|
89
|
+
|
|
90
|
+
ci_low = ci_high = None
|
|
91
|
+
# Bootstrapping a handful of points produces an interval that says nothing;
|
|
92
|
+
# reporting one anyway would imply precision that isn't there.
|
|
93
|
+
if confidence_intervals and len(values) >= 5:
|
|
94
|
+
ci_low, ci_high = bootstrap_ci(values, seed=seed)
|
|
95
|
+
|
|
96
|
+
return Metric(
|
|
97
|
+
key=key,
|
|
98
|
+
value=mean(values),
|
|
99
|
+
count=len(values),
|
|
100
|
+
error_count=error_count,
|
|
101
|
+
stddev=stddev(values),
|
|
102
|
+
ci_low=ci_low,
|
|
103
|
+
ci_high=ci_high,
|
|
104
|
+
slice=slice_,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _extra_slices(result: ExampleResult, slice_by: Sequence[str]) -> list[SliceKey]:
|
|
109
|
+
keys: list[SliceKey] = []
|
|
110
|
+
for dimension in slice_by:
|
|
111
|
+
value = result.metadata.get(dimension)
|
|
112
|
+
if value is None and result.expected is not None:
|
|
113
|
+
value = result.expected.get(dimension)
|
|
114
|
+
if value is not None:
|
|
115
|
+
keys.append(((dimension, str(value)),))
|
|
116
|
+
return keys
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _merge(a: SliceKey, b: SliceKey) -> SliceKey:
|
|
120
|
+
return tuple(sorted({**dict(a), **dict(b)}.items()))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def scores_for(results: Iterable[ExampleResult], metric_key: str) -> list[float]:
|
|
124
|
+
"""Every non-errored value for one metric, in result order.
|
|
125
|
+
|
|
126
|
+
Used by comparison to bootstrap a CI on the delta between two runs.
|
|
127
|
+
"""
|
|
128
|
+
return [
|
|
129
|
+
score.value
|
|
130
|
+
for result in results
|
|
131
|
+
for score in result.scores
|
|
132
|
+
if score.metric == metric_key and score.counts_toward_mean and score.value is not None
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def group_by_metric(scores: Iterable[Score]) -> dict[str, list[Score]]:
|
|
137
|
+
grouped: dict[str, list[Score]] = defaultdict(list)
|
|
138
|
+
for score in scores:
|
|
139
|
+
grouped[score.metric].append(score)
|
|
140
|
+
return dict(grouped)
|