Summary
Many scorers emit dict-valued Score.value (multiple numeric fields per sample) and then need per-key metrics at the task level. Today each eval re-implements that plumbing by hand, even when it's just "take the mean of foo across samples".
inspect_evals.utils.metrics.mean_of solves this for the specific case of mean aggregation. Per the Slack discussion, a more general aggregate(key, agg=mean) factory that composes an arbitrary aggregator with a key selector is a better fit for the upstream metric API.
Motivation
Current pattern in inspect_evals:
from inspect_ai.scorer import Metric, metric
from inspect_evals.utils import mean_of
@metric
def element_accuracy() -> Metric:
return mean_of("element_acc", on_missing="skip")
@metric
def action_f1() -> Metric:
return mean_of("action_f1", on_missing="skip")
Extending this to other aggregators (stderr, std, percentile, harmonic mean, ...) currently requires writing a new factory for each. A single composition primitive would cover all of them and would compose cleanly with the existing accuracy, mean, stderr metrics in inspect_ai.scorer.
Proposed API
from inspect_ai.scorer import Metric, aggregate, mean, stderr
@metric
def element_accuracy() -> Metric:
return aggregate("element_acc", agg=mean(), on_missing="skip")
@metric
def element_accuracy_stderr() -> Metric:
return aggregate("element_acc", agg=stderr(), on_missing="skip")
Signature sketch:
def aggregate(
key: str,
agg: Metric,
*,
to_float: ValueToFloat = value_to_float(),
on_missing: Literal["error", "skip", "zero"] = "error",
) -> Metric:
"""Apply `agg` to the `key` field extracted from each dict-valued
Score.value."""
Semantics:
- For each
SampleScore, require score.value to be a dict.
- Extract
value[key], convert with to_float, feed the resulting SampleScores (with unwrapped values) into agg.
on_missing controls how samples without key are handled:
"error" (default): raise.
"skip": exclude from the aggregator.
"zero": treat as 0.0.
Reference implementation
Current, mean-only version: src/inspect_evals/utils/metrics.py
mean_of(
key: str,
to_float: ValueToFloat = value_to_float(),
on_missing: Literal["error", "skip", "zero"] = "error",
) -> Metric
Related
Prior discussion
- Slack thread on Inspect Community
#feature-requests (2026-04-17).
- JJ: "Or maybe
aggregate(key, agg=mean)?" — preferred shape.
Summary
Many scorers emit dict-valued
Score.value(multiple numeric fields per sample) and then need per-key metrics at the task level. Today each eval re-implements that plumbing by hand, even when it's just "take the mean offooacross samples".inspect_evals.utils.metrics.mean_ofsolves this for the specific case of mean aggregation. Per the Slack discussion, a more generalaggregate(key, agg=mean)factory that composes an arbitrary aggregator with a key selector is a better fit for the upstream metric API.Motivation
Current pattern in
inspect_evals:Extending this to other aggregators (stderr, std, percentile, harmonic mean, ...) currently requires writing a new factory for each. A single composition primitive would cover all of them and would compose cleanly with the existing
accuracy,mean,stderrmetrics ininspect_ai.scorer.Proposed API
Signature sketch:
Semantics:
SampleScore, requirescore.valueto be a dict.value[key], convert withto_float, feed the resultingSampleScores (with unwrapped values) intoagg.on_missingcontrols how samples withoutkeyare handled:"error"(default): raise."skip": exclude from the aggregator."zero": treat as0.0.Reference implementation
Current, mean-only version:
src/inspect_evals/utils/metrics.pyRelated
inspect_evalsPR #770 review comment — raised independently in an eval PR, flagged as related during the Slack discussion.Prior discussion
#feature-requests(2026-04-17).aggregate(key, agg=mean)?" — preferred shape.