evalcore 2.0.0__tar.gz → 2.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.
- {evalcore-2.0.0 → evalcore-2.1.0}/CHANGELOG.md +38 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/PKG-INFO +1 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/pyproject.toml +1 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/graders/__init__.py +10 -1
- evalcore-2.1.0/src/evalcore/graders/base.py +145 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/graders/classification.py +1 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/graders/deterministic.py +4 -4
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/graders/judge.py +1 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/graders/numeric.py +1 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/store.py +9 -16
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_runner.py +2 -2
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_store.py +23 -1
- {evalcore-2.0.0 → evalcore-2.1.0}/uv.lock +1 -1
- evalcore-2.0.0/src/evalcore/graders/base.py +0 -78
- {evalcore-2.0.0 → evalcore-2.1.0}/.github/workflows/ci.yml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/.github/workflows/publish.yml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/.gitignore +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/.pre-commit-config.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/LICENSE +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/README.md +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/docs/design.md +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/README.md +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/adapter.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/graders.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/run_eval.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/suite.yaml +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/tests/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/examples/quickstart/tests/test_quickstart.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/justfile +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/pyrightconfig.json +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/adapters/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/adapters/base.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/adapters/env.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/adapters/http.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/adapters/replay.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/cli.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/compare.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/errors.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/loader.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/models.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/pairwise.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/py.typed +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/rating.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/refs.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/report.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/reporters/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/reporters/base.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/reporters/html.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/reporters/markdown.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/retry.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/runner.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/src/evalcore/sweep.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/__init__.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_adapters.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_cli.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_edge_cases.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_judge.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_judge_extra.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_live_clients.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_pairwise_extra.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_rating.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_rating_server.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_reporters.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_retry.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_sweep_pairwise.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/tests/test_unit.py +0 -0
- {evalcore-2.0.0 → evalcore-2.1.0}/uv.toml +0 -0
|
@@ -6,6 +6,41 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.1.0] - 2026-08-07
|
|
10
|
+
|
|
11
|
+
A grader declares what kind of check it is at registration, so a consumer
|
|
12
|
+
plug-in is categorised the same way a built-in is.
|
|
13
|
+
|
|
14
|
+
Shipped as a minor despite the signature change below. `register` is public,
|
|
15
|
+
so the 1.0.0 policy would call this a major; it goes out as 2.1.0 as a
|
|
16
|
+
deliberate exception, because the break is a one-line edit per grader that
|
|
17
|
+
fails loudly at import.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Breaking:** `graders.base.register` takes a required second argument,
|
|
21
|
+
`category`, a `graders.GraderType`. Every `@base.register('foo')` becomes
|
|
22
|
+
`@base.register('foo', base.GraderType.HEURISTIC)` or whichever member
|
|
23
|
+
applies; omitting it is a `TypeError` at import. Required rather than
|
|
24
|
+
defaulted on purpose - it is the only source of a row's `grader_type`, and
|
|
25
|
+
a default would be the value every grader forgets to override.
|
|
26
|
+
- `store.grader_lookups` reads the category from the registry instead of a
|
|
27
|
+
closed table of built-in type names, so a plug-in that declares
|
|
28
|
+
`HEURISTIC` reports `heuristic` where it used to report `unknown`. Rows for
|
|
29
|
+
consumer graders change value in the `grader_type` column; nothing about
|
|
30
|
+
the row shape changes.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `graders.GraderType`, a `StrEnum` over the closed set the results store's
|
|
34
|
+
`grader_type` column accepts: `unknown`, `heuristic`, `statistical`,
|
|
35
|
+
`llm_as_judge`, `trajectory`, `human`. A `StrEnum` so it needs no
|
|
36
|
+
serializer of its own on the way to a row.
|
|
37
|
+
- `graders.category_of`, the registry lookup behind `grader_lookups`.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
- `store._GRADER_TYPES`, the private table the categories used to live in.
|
|
41
|
+
Keeping it alongside the registration argument would mean two sources for
|
|
42
|
+
one fact and a precedence rule between them.
|
|
43
|
+
|
|
9
44
|
## [2.0.0] - 2026-07-31
|
|
10
45
|
|
|
11
46
|
Breaks both the public API and the outbox row shape, so it's a major per the
|
|
@@ -139,7 +174,9 @@ by semantic versioning: a breaking change to either means a 2.0.
|
|
|
139
174
|
rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
|
|
140
175
|
column-store outbox, and content-hash provenance.
|
|
141
176
|
|
|
142
|
-
[Unreleased]: https://github.com/scottpmiller/evalcore/compare/1.0
|
|
177
|
+
[Unreleased]: https://github.com/scottpmiller/evalcore/compare/2.1.0...HEAD
|
|
178
|
+
[2.1.0]: https://github.com/scottpmiller/evalcore/compare/2.0.0...2.1.0
|
|
179
|
+
[2.0.0]: https://github.com/scottpmiller/evalcore/compare/1.0.0...2.0.0
|
|
143
180
|
[1.0.0]: https://github.com/scottpmiller/evalcore/compare/0.3.0...1.0.0
|
|
144
181
|
[0.3.0]: https://github.com/scottpmiller/evalcore/compare/0.2.0...0.3.0
|
|
145
182
|
[0.2.0]: https://github.com/scottpmiller/evalcore/compare/0.1.0...0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evalcore
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/scottpmiller/evalcore
|
|
6
6
|
Project-URL: Repository, https://github.com/scottpmiller/evalcore
|
|
@@ -20,5 +20,14 @@ from evalcore.graders import (
|
|
|
20
20
|
judge,
|
|
21
21
|
numeric,
|
|
22
22
|
)
|
|
23
|
+
from evalcore.graders.base import GraderType, category_of
|
|
23
24
|
|
|
24
|
-
__all__ = [
|
|
25
|
+
__all__ = [
|
|
26
|
+
'GraderType',
|
|
27
|
+
'base',
|
|
28
|
+
'category_of',
|
|
29
|
+
'classification',
|
|
30
|
+
'deterministic',
|
|
31
|
+
'judge',
|
|
32
|
+
'numeric',
|
|
33
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Grader protocols and the type registry.
|
|
2
|
+
|
|
3
|
+
A grader spec is a plain dict from the suite config: ``{type, name, ...}``.
|
|
4
|
+
``build_graders`` turns a list of specs into grader instances, split into the
|
|
5
|
+
per-case and aggregate buckets the runner needs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from evalcore import models
|
|
12
|
+
from evalcore.errors import ConfigError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GraderType(enum.StrEnum):
|
|
16
|
+
"""What kind of check a grader performs, not which one.
|
|
17
|
+
|
|
18
|
+
A closed set, unlike the registry's ``type`` names, which any consumer
|
|
19
|
+
may extend. Declared once per grader at registration and read back by
|
|
20
|
+
``store.grader_lookups``; a ``StrEnum`` so it needs no serializer of its
|
|
21
|
+
own on the way to a row.
|
|
22
|
+
|
|
23
|
+
The same set is spelled out in three other places, all of which have to
|
|
24
|
+
change together: ``GraderType`` in ``internal-eval-results``, the
|
|
25
|
+
``grader_type`` ``Enum8`` in that repo's ``schema.sql``, and the deployed
|
|
26
|
+
DDL in ``schemata/clickhouse`` on GHE, which is the source of truth.
|
|
27
|
+
|
|
28
|
+
``UNKNOWN`` exists for a producer with no registry behind it. Nothing in
|
|
29
|
+
evalcore emits it: ``register`` requires a category, so a grader that
|
|
30
|
+
reaches a suite has always declared one.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
UNKNOWN = 'unknown'
|
|
35
|
+
HEURISTIC = 'heuristic'
|
|
36
|
+
STATISTICAL = 'statistical'
|
|
37
|
+
LLM_AS_JUDGE = 'llm_as_judge'
|
|
38
|
+
TRAJECTORY = 'trajectory'
|
|
39
|
+
HUMAN = 'human'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@typing.runtime_checkable
|
|
43
|
+
class Grader(typing.Protocol):
|
|
44
|
+
"""Per-case grader. Scores are averaged across cases by the runner."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
|
|
48
|
+
def grade(
|
|
49
|
+
self, case: models.Case, output: models.Output
|
|
50
|
+
) -> list[models.Score]: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@typing.runtime_checkable
|
|
54
|
+
class AggregateGrader(typing.Protocol):
|
|
55
|
+
"""Whole-run grader for set-level metrics (P/R/F1, win-rate, ...)."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
|
|
59
|
+
def aggregate(
|
|
60
|
+
self, results: list[models.CaseResult]
|
|
61
|
+
) -> list[models.Score]: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_REGISTRY: dict[str, type] = {}
|
|
65
|
+
|
|
66
|
+
_CATEGORIES: dict[str, GraderType] = {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def register(
|
|
70
|
+
type_name: str, category: GraderType
|
|
71
|
+
) -> typing.Callable[[type], type]:
|
|
72
|
+
"""Class decorator registering a grader under a suite-config ``type``.
|
|
73
|
+
|
|
74
|
+
``category`` is required rather than defaulting, because it is the only
|
|
75
|
+
source of the row's ``grader_type`` and a default would be the value
|
|
76
|
+
every grader forgets to override. A grader's category belongs to its
|
|
77
|
+
implementation, not to a suite's use of it, so it is declared here and
|
|
78
|
+
not in the suite config.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
type_name: The ``type`` a suite spec names to select this grader.
|
|
82
|
+
category: What kind of check it performs.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The decorator.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ConfigError: If ``type_name`` is already registered.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def _decorate(cls: type) -> type:
|
|
93
|
+
if type_name in _REGISTRY:
|
|
94
|
+
raise ConfigError(f'grader type {type_name!r} already registered')
|
|
95
|
+
_REGISTRY[type_name] = cls
|
|
96
|
+
_CATEGORIES[type_name] = GraderType(category)
|
|
97
|
+
return cls
|
|
98
|
+
|
|
99
|
+
return _decorate
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def category_of(type_name: str) -> GraderType:
|
|
103
|
+
"""Return the category a grader type registered under.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
type_name: The suite spec's ``type``.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The declared category, or ``UNKNOWN`` for a type no plug-in has
|
|
110
|
+
registered. A suite naming one cannot run - ``build_graders``
|
|
111
|
+
raises - so ``UNKNOWN`` only reaches a row when a caller builds
|
|
112
|
+
rows without loading the plug-ins that produced them.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
return _CATEGORIES.get(type_name, GraderType.UNKNOWN)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_graders(
|
|
119
|
+
specs: list[dict],
|
|
120
|
+
) -> tuple[list[Grader], list[AggregateGrader]]:
|
|
121
|
+
"""Instantiate grader specs, partitioned into per-case and aggregate.
|
|
122
|
+
|
|
123
|
+
Each spec's ``type`` selects a registered class; remaining keys (minus
|
|
124
|
+
``type``) are passed as keyword arguments to its constructor.
|
|
125
|
+
"""
|
|
126
|
+
per_case: list[Grader] = []
|
|
127
|
+
aggregate: list[AggregateGrader] = []
|
|
128
|
+
for spec in specs:
|
|
129
|
+
spec = dict(spec)
|
|
130
|
+
type_name = spec.pop('type')
|
|
131
|
+
if type_name not in _REGISTRY:
|
|
132
|
+
raise ConfigError(
|
|
133
|
+
f'unknown grader type {type_name!r}; '
|
|
134
|
+
f'known: {sorted(_REGISTRY)}'
|
|
135
|
+
)
|
|
136
|
+
grader = _REGISTRY[type_name](**spec)
|
|
137
|
+
if isinstance(grader, AggregateGrader):
|
|
138
|
+
aggregate.append(grader)
|
|
139
|
+
elif isinstance(grader, Grader):
|
|
140
|
+
per_case.append(grader)
|
|
141
|
+
else: # pragma: no cover - defensive
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f'{type_name!r} is neither Grader nor AggregateGrader'
|
|
144
|
+
)
|
|
145
|
+
return per_case, aggregate
|
|
@@ -19,7 +19,7 @@ def _safe_div(numerator: float, denominator: float) -> float:
|
|
|
19
19
|
return numerator / denominator if denominator else 0.0
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
@base.register('classification')
|
|
22
|
+
@base.register('classification', base.GraderType.STATISTICAL)
|
|
23
23
|
class Classification:
|
|
24
24
|
"""Binary precision/recall/F1 + FN/FP rates over a labeled dataset."""
|
|
25
25
|
|
|
@@ -32,7 +32,7 @@ def _score(name: str, metric: str, case_id: str, ok: bool, detail: str):
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
@base.register('max_chars')
|
|
35
|
+
@base.register('max_chars', base.GraderType.HEURISTIC)
|
|
36
36
|
class MaxChars:
|
|
37
37
|
"""Assert a text field is at most ``maximum`` characters long."""
|
|
38
38
|
|
|
@@ -58,7 +58,7 @@ class MaxChars:
|
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
@base.register('regex_absent')
|
|
61
|
+
@base.register('regex_absent', base.GraderType.HEURISTIC)
|
|
62
62
|
class RegexAbsent:
|
|
63
63
|
"""Assert a text field does NOT match ``pattern`` (e.g. no tokens)."""
|
|
64
64
|
|
|
@@ -78,7 +78,7 @@ class RegexAbsent:
|
|
|
78
78
|
return [_score(self.name, self.name, case.id, ok, detail)]
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
@base.register('regex_present')
|
|
81
|
+
@base.register('regex_present', base.GraderType.HEURISTIC)
|
|
82
82
|
class RegexPresent:
|
|
83
83
|
"""Assert a text field matches EVERY pattern in ``patterns`` (all-of).
|
|
84
84
|
|
|
@@ -111,7 +111,7 @@ class RegexPresent:
|
|
|
111
111
|
return [_score(self.name, self.name, case.id, ok, detail)]
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
@base.register('non_empty')
|
|
114
|
+
@base.register('non_empty', base.GraderType.HEURISTIC)
|
|
115
115
|
class NonEmpty:
|
|
116
116
|
"""Assert a field resolves to a non-empty value."""
|
|
117
117
|
|
|
@@ -289,7 +289,7 @@ def _load_images(refs_values: list) -> list[dict]:
|
|
|
289
289
|
return images
|
|
290
290
|
|
|
291
291
|
|
|
292
|
-
@base.register('llm_judge')
|
|
292
|
+
@base.register('llm_judge', base.GraderType.LLM_AS_JUDGE)
|
|
293
293
|
class RubricJudge:
|
|
294
294
|
"""Score an output's text on rubric dimensions with an LLM judge/panel.
|
|
295
295
|
|
|
@@ -33,7 +33,7 @@ than model attribute names, so ``project`` is emitted as ``application``,
|
|
|
33
33
|
import json
|
|
34
34
|
import pathlib
|
|
35
35
|
|
|
36
|
-
from evalcore import models
|
|
36
|
+
from evalcore import graders, models
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def write_scorecard(
|
|
@@ -210,25 +210,18 @@ def read_preferences(path: str | pathlib.Path) -> list[models.Preference]:
|
|
|
210
210
|
]
|
|
211
211
|
|
|
212
212
|
|
|
213
|
-
_GRADER_TYPES = {
|
|
214
|
-
'classification': 'statistical',
|
|
215
|
-
'llm_judge': 'llm_as_judge',
|
|
216
|
-
'max_chars': 'heuristic',
|
|
217
|
-
'non_empty': 'heuristic',
|
|
218
|
-
'numeric': 'heuristic',
|
|
219
|
-
'regex_absent': 'heuristic',
|
|
220
|
-
'regex_present': 'heuristic',
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
213
|
def grader_lookups(specs: list[dict]) -> tuple[dict[str, str], dict[str, int]]:
|
|
225
214
|
"""Grader name to category, and grader name to judge scale.
|
|
226
215
|
|
|
227
216
|
A ``Score`` names the grader that emitted it but carries neither the
|
|
228
217
|
grader's category nor the scale its raw judge points sit on, so the caller
|
|
229
|
-
builds both from ``suite.graders`` and hands them to :func:`score_rows`.
|
|
230
|
-
|
|
231
|
-
|
|
218
|
+
builds both from ``suite.graders`` and hands them to :func:`score_rows`.
|
|
219
|
+
|
|
220
|
+
The category comes from the registry, where each grader declared it at
|
|
221
|
+
registration, so a consumer plug-in is categorised the same way a built-in
|
|
222
|
+
is. A type nothing has registered lands on ``'unknown'`` rather than being
|
|
223
|
+
guessed at, which in practice means the caller built rows without
|
|
224
|
+
importing the plug-ins: a suite naming an unregistered type cannot run.
|
|
232
225
|
"""
|
|
233
226
|
types: dict[str, str] = {}
|
|
234
227
|
scales: dict[str, int] = {}
|
|
@@ -237,7 +230,7 @@ def grader_lookups(specs: list[dict]) -> tuple[dict[str, str], dict[str, int]]:
|
|
|
237
230
|
if not name:
|
|
238
231
|
continue
|
|
239
232
|
grader_type = spec.get('type', '')
|
|
240
|
-
types[name] =
|
|
233
|
+
types[name] = str(graders.category_of(grader_type))
|
|
241
234
|
if grader_type == 'llm_judge':
|
|
242
235
|
scales[name] = int(spec.get('scale', 5))
|
|
243
236
|
return types, scales
|
|
@@ -33,7 +33,7 @@ class _RecordingAdapter:
|
|
|
33
33
|
self.closed = True
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
@graders_base.register('_modewatch')
|
|
36
|
+
@graders_base.register('_modewatch', graders_base.GraderType.HEURISTIC)
|
|
37
37
|
class _ModeWatchGrader:
|
|
38
38
|
"""A per-case grader that records the run mode via set_mode."""
|
|
39
39
|
|
|
@@ -60,7 +60,7 @@ class _ModeWatchGrader:
|
|
|
60
60
|
]
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
@graders_base.register('_pin')
|
|
63
|
+
@graders_base.register('_pin', graders_base.GraderType.HEURISTIC)
|
|
64
64
|
class _PinGrader:
|
|
65
65
|
"""A grader exposing a judge_version pin, like the LLM judge does."""
|
|
66
66
|
|
|
@@ -5,7 +5,7 @@ import pathlib
|
|
|
5
5
|
import tempfile
|
|
6
6
|
import unittest
|
|
7
7
|
|
|
8
|
-
from evalcore import models, store
|
|
8
|
+
from evalcore import errors, graders, models, store
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _run(with_failure: bool = False):
|
|
@@ -315,6 +315,28 @@ class GraderLookupTests(unittest.TestCase):
|
|
|
315
315
|
types, _ = store.grader_lookups([{'type': 'non_empty'}])
|
|
316
316
|
self.assertEqual(types['non_empty'], 'heuristic')
|
|
317
317
|
|
|
318
|
+
def test_a_plugin_is_categorised_like_a_built_in(self):
|
|
319
|
+
"""The point of the registry lookup: a consumer grader that declares
|
|
320
|
+
its category reads back as that category, not as 'unknown'.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
@graders.base.register('_store_plugin', graders.GraderType.HEURISTIC)
|
|
324
|
+
class _Plugin:
|
|
325
|
+
name = '_store_plugin'
|
|
326
|
+
|
|
327
|
+
def grade(self, case, output): # pragma: no cover - not run
|
|
328
|
+
return []
|
|
329
|
+
|
|
330
|
+
types, _ = store.grader_lookups([{'type': '_store_plugin'}])
|
|
331
|
+
self.assertEqual(types['_store_plugin'], 'heuristic')
|
|
332
|
+
|
|
333
|
+
def test_a_type_cannot_register_twice(self):
|
|
334
|
+
with self.assertRaises(errors.ConfigError):
|
|
335
|
+
|
|
336
|
+
@graders.base.register('non_empty', graders.GraderType.HEURISTIC)
|
|
337
|
+
class _Clash:
|
|
338
|
+
name = 'non_empty'
|
|
339
|
+
|
|
318
340
|
|
|
319
341
|
class ExporterTests(unittest.TestCase):
|
|
320
342
|
def test_export_scores(self):
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"""Grader protocols and the type registry.
|
|
2
|
-
|
|
3
|
-
A grader spec is a plain dict from the suite config: ``{type, name, ...}``.
|
|
4
|
-
``build_graders`` turns a list of specs into grader instances, split into the
|
|
5
|
-
per-case and aggregate buckets the runner needs.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import typing
|
|
9
|
-
|
|
10
|
-
from evalcore import models
|
|
11
|
-
from evalcore.errors import ConfigError
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@typing.runtime_checkable
|
|
15
|
-
class Grader(typing.Protocol):
|
|
16
|
-
"""Per-case grader. Scores are averaged across cases by the runner."""
|
|
17
|
-
|
|
18
|
-
name: str
|
|
19
|
-
|
|
20
|
-
def grade(
|
|
21
|
-
self, case: models.Case, output: models.Output
|
|
22
|
-
) -> list[models.Score]: ...
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@typing.runtime_checkable
|
|
26
|
-
class AggregateGrader(typing.Protocol):
|
|
27
|
-
"""Whole-run grader for set-level metrics (P/R/F1, win-rate, ...)."""
|
|
28
|
-
|
|
29
|
-
name: str
|
|
30
|
-
|
|
31
|
-
def aggregate(
|
|
32
|
-
self, results: list[models.CaseResult]
|
|
33
|
-
) -> list[models.Score]: ...
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_REGISTRY: dict[str, type] = {}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def register(type_name: str) -> typing.Callable[[type], type]:
|
|
40
|
-
"""Class decorator registering a grader under a suite-config ``type``."""
|
|
41
|
-
|
|
42
|
-
def _decorate(cls: type) -> type:
|
|
43
|
-
if type_name in _REGISTRY:
|
|
44
|
-
raise ConfigError(f'grader type {type_name!r} already registered')
|
|
45
|
-
_REGISTRY[type_name] = cls
|
|
46
|
-
return cls
|
|
47
|
-
|
|
48
|
-
return _decorate
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def build_graders(
|
|
52
|
-
specs: list[dict],
|
|
53
|
-
) -> tuple[list[Grader], list[AggregateGrader]]:
|
|
54
|
-
"""Instantiate grader specs, partitioned into per-case and aggregate.
|
|
55
|
-
|
|
56
|
-
Each spec's ``type`` selects a registered class; remaining keys (minus
|
|
57
|
-
``type``) are passed as keyword arguments to its constructor.
|
|
58
|
-
"""
|
|
59
|
-
per_case: list[Grader] = []
|
|
60
|
-
aggregate: list[AggregateGrader] = []
|
|
61
|
-
for spec in specs:
|
|
62
|
-
spec = dict(spec)
|
|
63
|
-
type_name = spec.pop('type')
|
|
64
|
-
if type_name not in _REGISTRY:
|
|
65
|
-
raise ConfigError(
|
|
66
|
-
f'unknown grader type {type_name!r}; '
|
|
67
|
-
f'known: {sorted(_REGISTRY)}'
|
|
68
|
-
)
|
|
69
|
-
grader = _REGISTRY[type_name](**spec)
|
|
70
|
-
if isinstance(grader, AggregateGrader):
|
|
71
|
-
aggregate.append(grader)
|
|
72
|
-
elif isinstance(grader, Grader):
|
|
73
|
-
per_case.append(grader)
|
|
74
|
-
else: # pragma: no cover - defensive
|
|
75
|
-
raise TypeError(
|
|
76
|
-
f'{type_name!r} is neither Grader nor AggregateGrader'
|
|
77
|
-
)
|
|
78
|
-
return per_case, aggregate
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|