Skip to content

Commit 71c96fe

Browse files
ajay-kesavanclaude
andauthored
feat(eval): dataset-level evaluators + classification schemas, samples, e2e (#1663)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7be0e7d commit 71c96fe

11 files changed

Lines changed: 1665 additions & 5 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Aggregator specs embedded in per-datapoint classification evaluator configs.
2+
3+
Each aggregator is a run-level metric (precision / recall / f-score) attached
4+
to a classification evaluator. Classes are declared once on the parent evaluator
5+
config — every aggregator on the same evaluator operates on the same class
6+
vocabulary, so the field is not repeated per spec. Only the metric-shape fields
7+
(``averaging`` and, for fscore, ``f_value``) live on the spec itself.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from typing import Annotated, Literal, Union
13+
14+
from pydantic import BaseModel, ConfigDict, Field
15+
from pydantic.alias_generators import to_camel
16+
17+
18+
class _AggregatorSpecBase(BaseModel):
19+
"""Shared pydantic config for every aggregator variant."""
20+
21+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
22+
23+
24+
class PrecisionAggregatorSpec(_AggregatorSpecBase):
25+
"""Run-level precision aggregator (multiclass, micro or macro averaged)."""
26+
27+
type: Literal["precision"] = "precision"
28+
averaging: Literal["macro", "micro"]
29+
30+
31+
class RecallAggregatorSpec(_AggregatorSpecBase):
32+
"""Run-level recall aggregator (multiclass, micro or macro averaged)."""
33+
34+
type: Literal["recall"] = "recall"
35+
averaging: Literal["macro", "micro"]
36+
37+
38+
class FScoreAggregatorSpec(_AggregatorSpecBase):
39+
"""Run-level F-beta aggregator (multiclass, micro or macro averaged)."""
40+
41+
type: Literal["fscore"] = "fscore"
42+
averaging: Literal["macro", "micro"]
43+
# Upper bound keeps beta² finite — a huge beta overflows to inf and the
44+
# F-score becomes NaN, which is not representable in JSON.
45+
f_value: float = Field(default=1.0, gt=0, le=1000)
46+
47+
48+
class ConfusionMatrixAggregatorSpec(_AggregatorSpecBase):
49+
"""Run-level raw k×k confusion matrix — no scalar headline, no averaging."""
50+
51+
type: Literal["confusion_matrix"] = "confusion_matrix"
52+
53+
54+
AggregatorSpec = Annotated[
55+
Union[
56+
PrecisionAggregatorSpec,
57+
RecallAggregatorSpec,
58+
FScoreAggregatorSpec,
59+
ConfusionMatrixAggregatorSpec,
60+
],
61+
Field(discriminator="type"),
62+
]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Base abstractions for dataset-level evaluators.
2+
3+
A dataset-level evaluator runs once per evaluation set, after all per-datapoint
4+
evaluators have produced their results. It consumes the per-datapoint
5+
EvaluationResultDto values from one named source evaluator and emits a single
6+
EvaluationResult that summarizes the dataset.
7+
8+
Concretely distinct from GenericBaseEvaluator: different evaluate() signature,
9+
different lifecycle. Kept as a parallel hierarchy rather than a subclass so the
10+
runtime cannot accidentally dispatch a dataset evaluator through the
11+
per-datapoint loop.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from abc import ABC, abstractmethod
17+
18+
from ..models.models import EvaluationResult, EvaluationResultDto
19+
from ._aggregator_specs import AggregatorSpec
20+
21+
22+
class BaseDatasetEvaluator(ABC):
23+
"""Abstract base for dataset-level evaluators.
24+
25+
Constructed from an :class:`AggregatorSpec`, the source evaluator's name,
26+
and the class vocabulary of the parent per-datapoint evaluator. Classes
27+
live on the evaluator config (not the spec) — every aggregator on the same
28+
evaluator operates on the same vocabulary.
29+
"""
30+
31+
spec: AggregatorSpec
32+
source_evaluator: str
33+
classes: list[str]
34+
35+
def __init__(
36+
self, spec: AggregatorSpec, source_evaluator: str, classes: list[str]
37+
) -> None:
38+
"""Store the aggregator spec, source evaluator name, and shared classes."""
39+
self.spec = spec
40+
self.source_evaluator = source_evaluator
41+
self.classes = classes
42+
43+
@abstractmethod
44+
def evaluate(self, results: list[EvaluationResultDto]) -> EvaluationResult:
45+
"""Reduce per-datapoint results into a single run-level EvaluationResult."""

packages/uipath/src/uipath/eval/evaluators/base_evaluator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ class BaseEvaluatorJustification(BaseModel):
4747
expected: str
4848
actual: str
4949

50+
@classmethod
51+
def try_from(cls, details: object) -> "BaseEvaluatorJustification | None":
52+
"""Coerce a free-form details payload into a justification, or return None.
53+
54+
Accepts either an existing instance or a dict that ``model_validate`` can
55+
parse. Anything else (str, None, malformed dict) yields ``None``.
56+
"""
57+
if isinstance(details, cls):
58+
return details
59+
if isinstance(details, dict):
60+
try:
61+
return cls.model_validate(details)
62+
except Exception:
63+
return None
64+
return None
65+
5066

5167
# Additional type variables for Config and Justification
5268
# Note: C must be BaseEvaluatorConfig[T] to ensure type consistency

0 commit comments

Comments
 (0)