Skip to content

Commit 740c115

Browse files
committed
refactor(evaluator)!: unify the backend contract on evaluate/evaluate_dataset
The SDK backend protocol carried two dataset entrypoints that differed only in metric arity and return type: `evaluate` took one metric and returned an `EvaluationResult`, `evaluate_benchmark` took a list and returned a `BenchmarkEvaluationResult`. One metric is the degenerate case of several, so collapse them into a single `evaluate_dataset(metrics=[...])` and free the `evaluate` name for the taskset entrypoint. `Evaluator.run`/`run_sync` lose their single-metric overloads; callers pass a list. `BenchmarkEvaluationResult` gains the `__str__` that `EvaluationResult` had, so `print(result)` stays readable through the collapsed path. Introduce a job-handle contract. `EvaluationJob`/`SyncEvaluationJob` declare `wait_until_done` and `get_result`, so a backend that runs work elsewhere hands back a handle and the caller chooses when to wait; in-process execution returns an already-finished `CompletedJob`. `Evaluator.submit` waits on the caller's behalf, so the convenience API still returns a result either way. The plugin's job resources satisfy that contract structurally, so both `evaluate` and `evaluate_dataset` now return handles and the resource is consistent. The agent-eval handle carries the taskset it submitted: rebuilding the result needs the caller's live tasks, because persisted task metrics serialize as descriptors that cannot be validated back into `Metric` objects, and holding them on the handle means no caller has to know that. Polling and bundle reassembly move out of the executor onto the handle, which drops the duplicated URL, wait, and download logic from both executor flavours and replaces two hard-coded timeout constants with the three the dataset handles already take. Retire plugin-local execution. `client.evaluator.run()` is gone and `submit` is now `evaluate_dataset`, taking a metric list. That orphaned the executor's local paths, so remove `run_local`, `evaluate`, `evaluate_benchmark` and `evaluate_remote` from both executors along with the two metric type-guards whose only purpose was the single-vs-sequence dispatch. Reject two silent failures the agent-eval path allowed: a task with no instruction (the wire schema permits null, so a job would reach the agent with nothing to do) and run params that do not match the target kind (previously dropped, so a caller's parallelism vanished without a word). BREAKING CHANGE: `EvaluationBackend.evaluate` now takes a taskset and returns an `EvaluationJob`; dataset evaluation moves to `evaluate_dataset` with a metric list. `Evaluator.run`/`run_sync` no longer accept a bare metric. `client.evaluator.run()` is removed, `client.evaluator.submit()` is now `client.evaluator.evaluate_dataset()`, and `client.evaluator.evaluate()` returns a job handle rather than a completed result. Signed-off-by: Sandy Chapman <schapman@nvidia.com>
1 parent 0c4dc81 commit 740c115

46 files changed

Lines changed: 2425 additions & 1852 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e/test_evaluator_plugin.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,8 @@ def evaluator_sdk(sdk: NeMoPlatform, evaluator_workspace: str) -> Iterator[NeMoP
352352

353353
@pytest.fixture(scope="module")
354354
def completed_offline_job(evaluator_sdk: NeMoPlatform) -> Iterator[EvaluatorJobResource]:
355-
job = evaluator_sdk.evaluator.submit(
356-
metric=_exact_match_metric(),
355+
job = evaluator_sdk.evaluator.evaluate_dataset(
356+
metrics=[_exact_match_metric()],
357357
dataset=_offline_rows(),
358358
config=RunConfig(parallelism=1),
359359
)
@@ -479,8 +479,8 @@ def test_fileset_fragment_and_glob_datasets(evaluator_sdk: NeMoPlatform) -> None
479479
"glob": (f"{workspace}/{fileset_name}#part-*.json", [1.0, 0.0, 1.0]),
480480
}
481481
for label, (reference, expected_scores) in cases.items():
482-
job = evaluator_sdk.evaluator.submit(
483-
metric=_exact_match_metric(),
482+
job = evaluator_sdk.evaluator.evaluate_dataset(
483+
metrics=[_exact_match_metric()],
484484
dataset=FilesetRef(root=reference),
485485
config=RunConfig(parallelism=1),
486486
)
@@ -500,8 +500,8 @@ def test_fileset_fragment_and_glob_datasets(evaluator_sdk: NeMoPlatform) -> None
500500

501501
def test_run_config_limits_samples(evaluator_sdk: NeMoPlatform) -> None:
502502
rows = [{"expected": str(index), "output": str(index)} for index in range(8)]
503-
job = evaluator_sdk.evaluator.submit(
504-
metric=_exact_match_metric(),
503+
job = evaluator_sdk.evaluator.evaluate_dataset(
504+
metrics=[_exact_match_metric()],
505505
dataset=rows,
506506
config=RunConfig(limit_samples=3, parallelism=2),
507507
)
@@ -596,8 +596,8 @@ def test_tool_calling_metric_preserves_structured_references(evaluator_sdk: NeMo
596596
},
597597
},
598598
]
599-
job = evaluator_sdk.evaluator.submit(
600-
metric=ToolCallingMetric(reference="{{item.expected_tool_calls}}"),
599+
job = evaluator_sdk.evaluator.evaluate_dataset(
600+
metrics=[ToolCallingMetric(reference="{{item.expected_tool_calls}}")],
601601
dataset=rows,
602602
config=RunConfig(parallelism=2),
603603
)
@@ -629,8 +629,8 @@ def test_online_evaluate_job_uses_mock_provider(
629629
format=ModelFormat.OPEN_AI,
630630
)
631631

632-
job = evaluator_sdk.evaluator.submit(
633-
metric=_exact_match_metric(candidate=None),
632+
job = evaluator_sdk.evaluator.evaluate_dataset(
633+
metrics=[_exact_match_metric(candidate=None)],
634634
dataset=[{"question": "What is the capital of France?", "expected": "Paris"}],
635635
config=RunConfigOnlineModel(
636636
parallelism=1,
@@ -683,8 +683,8 @@ def test_llm_judge_metric_resolves_model_ref(
683683
]
684684
},
685685
)
686-
job = evaluator_sdk.evaluator.submit(
687-
metric=metric,
686+
job = evaluator_sdk.evaluator.evaluate_dataset(
687+
metrics=[metric],
688688
dataset=[{"answer": "Paris"}],
689689
config=RunConfig(parallelism=1),
690690
)
@@ -701,8 +701,8 @@ def _assert_runtime_input_failure(
701701
metric: StringCheckMetric | ExactMatchMetric,
702702
dataset: list[dict[str, object]] | FilesetRef,
703703
) -> None:
704-
job = evaluator_sdk.evaluator.submit(
705-
metric=metric,
704+
job = evaluator_sdk.evaluator.evaluate_dataset(
705+
metrics=[metric],
706706
dataset=dataset,
707707
config=RunConfig(parallelism=1),
708708
)

packages/nemo_evaluator_sdk/examples/examples.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ async def run_offline_local_exact_match_example() -> None:
360360
print("Running offline exact match...")
361361

362362
exact_match_result = await evaluator.run(
363-
metrics=exact_match,
363+
metrics=[exact_match],
364364
dataset=OFFLINE_EXACT_MATCH_DATASET,
365365
config=RunConfig(parallelism=4),
366366
)
@@ -382,7 +382,7 @@ async def run_online_local_exact_match_example() -> None:
382382
print("Running local online exact match...")
383383

384384
exact_match_result = await evaluator.run(
385-
metrics=exact_match,
385+
metrics=[exact_match],
386386
target=model,
387387
dataset=ONLINE_EXACT_MATCH_DATASET,
388388
prompt_template=ONLINE_CHAT_PROMPT_TEMPLATE,
@@ -523,7 +523,7 @@ async def run_local_metric_with_template_failure_example() -> None:
523523
print("\nRunning local metric evaluation with an invalid metric template...")
524524
try:
525525
await evaluator.run(
526-
metrics=invalid_metric,
526+
metrics=[invalid_metric],
527527
dataset=dataset,
528528
config=RunConfig(parallelism=1),
529529
)
@@ -553,7 +553,7 @@ async def run_offline_local_llm_judge_example() -> None:
553553
print("\nRunning local LLM judge evaluation...")
554554

555555
llm_judge_result = await evaluator.run(
556-
metrics=llm_judge_metric,
556+
metrics=[llm_judge_metric],
557557
dataset=OFFLINE_JUDGE_DATASET,
558558
config=RunConfig(parallelism=2),
559559
)
@@ -575,7 +575,7 @@ async def run_online_local_llm_judge_example() -> None:
575575
print("\nRunning local online LLM judge evaluation...")
576576

577577
llm_judge_result = await evaluator.run(
578-
metrics=llm_judge_metric,
578+
metrics=[llm_judge_metric],
579579
target=model_with_custom_headers,
580580
dataset=ONLINE_JUDGE_DATASET,
581581
prompt_template=ONLINE_CHAT_PROMPT_TEMPLATE,
@@ -593,7 +593,7 @@ def run_sync_example() -> None:
593593

594594
evaluator = Evaluator()
595595
result = evaluator.run_sync(
596-
metrics=ExactMatchMetric(reference="{{item.reference}}", candidate="{{item.actual}}"),
596+
metrics=[ExactMatchMetric(reference="{{item.reference}}", candidate="{{item.actual}}")],
597597
dataset=OFFLINE_EXACT_MATCH_DATASET[:1], # Only run the first sample
598598
config=RunConfig(parallelism=1),
599599
)

packages/nemo_evaluator_sdk/examples/high_level_evaluate_walkthrough.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@
349349
"]\n",
350350
"\n",
351351
"metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ model_output }}\")\n",
352-
"inline_result = evaluator.run_sync(metrics=metric, dataset=inline_rows)\n",
352+
"inline_result = evaluator.run_sync(metrics=[metric], dataset=inline_rows)\n",
353353
"\n",
354354
"runs[\"Inline list[dict]\"] = inline_result\n",
355355
"\n",
@@ -383,7 +383,7 @@
383383
")\n",
384384
"\n",
385385
"metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ model_output }}\")\n",
386-
"dataset_rows_result = evaluator.run_sync(metrics=metric, dataset=dataset_rows)\n",
386+
"dataset_rows_result = evaluator.run_sync(metrics=[metric], dataset=dataset_rows)\n",
387387
"\n",
388388
"runs[\"DatasetRows\"] = dataset_rows_result\n",
389389
"\n",
@@ -410,7 +410,7 @@
410410
"arrow_table = pa.Table.from_pylist(base_rows)\n",
411411
"\n",
412412
"metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ prediction }}\")\n",
413-
"arrow_result = evaluator.run_sync(metrics=metric, dataset=arrow_table)\n",
413+
"arrow_result = evaluator.run_sync(metrics=[metric], dataset=arrow_table)\n",
414414
"\n",
415415
"runs[\"pyarrow.Table\"] = arrow_result\n",
416416
"\n",
@@ -435,7 +435,7 @@
435435
"outputs": [],
436436
"source": [
437437
"metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ prediction }}\")\n",
438-
"file_result = evaluator.run_sync(metrics=metric, dataset=file_path)\n",
438+
"file_result = evaluator.run_sync(metrics=[metric], dataset=file_path)\n",
439439
"\n",
440440
"runs[\"File path\"] = file_result\n",
441441
"\n",
@@ -460,7 +460,7 @@
460460
"outputs": [],
461461
"source": [
462462
"metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ prediction }}\")\n",
463-
"directory_result = evaluator.run_sync(metrics=metric, dataset=dataset_dir, pattern=\"part-*.jsonl\")\n",
463+
"directory_result = evaluator.run_sync(metrics=[metric], dataset=dataset_dir, pattern=\"part-*.jsonl\")\n",
464464
"\n",
465465
"runs[\"Directory + pattern\"] = directory_result\n",
466466
"\n",

packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/evaluator.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@
8383
)
8484

8585

86+
def validate_run_inputs(
87+
*,
88+
tasks: Sequence[AgentEvalTask],
89+
trials: Sequence[AgentEvalTrial] | None,
90+
target: AgentEvalTarget | None,
91+
) -> None:
92+
"""Check the seams a run needs before any work starts.
93+
94+
Shared with the local backend so that a malformed taskset is rejected when the evaluation is
95+
requested, not when it is awaited — the same moment the remote path rejects it, in
96+
``build_spec`` before the job is created.
97+
98+
Raises:
99+
ValueError: If there are no tasks, or if neither or both of ``trials`` and ``target``
100+
were supplied.
101+
"""
102+
if not tasks:
103+
raise ValueError("at least one task is required")
104+
if (trials is None) == (target is None):
105+
raise ValueError("provide exactly one of trials or target")
106+
107+
86108
class AgentEvaluator:
87109
"""Run stored-trial or live-target agent evaluations.
88110
@@ -154,8 +176,7 @@ async def run(
154176
"""
155177
resolved_config = config or AgentEvalRunConfig()
156178
task_list = list(tasks)
157-
if not task_list:
158-
raise ValueError("at least one task is required")
179+
validate_run_inputs(tasks=task_list, trials=trials, target=target)
159180

160181
run_id = resolved_config.run_id or _new_run_id()
161182
runtime_config = resolved_config.model_copy(update={"run_id": run_id})

packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/execution/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The execution package exposes a single public entrypoint:
2222
# Local SDK execution
2323
evaluator = Evaluator()
2424
result = await evaluator.run(
25-
metrics=ExactMatchMetric(reference="{{item.reference}}"),
25+
metrics=[ExactMatchMetric(reference="{{item.reference}}")],
2626
dataset=[{"reference": "Paris", "output_text": "Paris"}],
2727
)
2828
```

packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/execution/backends/base.py

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77

88
from collections.abc import Sequence
99
from pathlib import Path
10-
from typing import Any, Protocol
10+
from typing import Any, Protocol, runtime_checkable
1111

12+
from nemo_evaluator_sdk.agent_eval.results import AgentEvalResult
13+
from nemo_evaluator_sdk.agent_eval.tasks import AgentEvalRunConfig, AgentEvalTask
14+
from nemo_evaluator_sdk.agent_eval.trials import AgentEvalTarget, AgentEvalTrial
15+
from nemo_evaluator_sdk.execution.jobs import EvaluationJob, SyncEvaluationJob
1216
from nemo_evaluator_sdk.inference import PostprocessResponse, PreprocessRequest
1317
from nemo_evaluator_sdk.metrics.protocol import Metric
1418
from nemo_evaluator_sdk.values import (
@@ -21,44 +25,50 @@
2125
RunConfigOnlineModel,
2226
)
2327
from nemo_evaluator_sdk.values.multi_metric_results import BenchmarkEvaluationResult
24-
from nemo_evaluator_sdk.values.results import AggregateFieldName, EvaluationResult
28+
from nemo_evaluator_sdk.values.results import AggregateFieldName
2529

2630
BackendParams = RunConfig | RunConfigOnline | RunConfigOnlineModel
2731

2832

33+
@runtime_checkable
2934
class EvaluationBackend(Protocol):
3035
async def evaluate(
3136
self,
3237
*,
33-
metric: Metric,
34-
dataset: DatasetInput | str | Path,
35-
params: BackendParams,
36-
target: Model | Agent | None = None,
37-
field_mapping: FieldMapping | None = None,
38-
prompt_template: str | dict[str, Any] | None = None,
39-
aggregate_fields: tuple[AggregateFieldName, ...] | None = None,
40-
preprocess_hooks: tuple[PreprocessRequest, ...] | None = None,
41-
postprocess_hooks: tuple[PostprocessResponse, ...] | None = None,
42-
) -> EvaluationResult:
43-
"""Evaluate one metric directly and return the completed result.
38+
taskset: Sequence[AgentEvalTask],
39+
target: AgentEvalTarget | None = None,
40+
trials: Sequence[AgentEvalTrial] | None = None,
41+
config: AgentEvalRunConfig | None = None,
42+
) -> EvaluationJob[AgentEvalResult]:
43+
"""Start evaluating a taskset — tasks that each carry their own metrics — and return its job.
44+
45+
The entrypoint ``evaluate_dataset`` is intended to fold into: a dataset with one shared
46+
metric list is a taskset whose metrics have been hoisted. That is not implemented yet — this
47+
method cannot express a dataset today — so ``evaluate_dataset`` remains the way to run one,
48+
and is not deprecated.
49+
50+
Returns a job rather than a result so the caller chooses when to wait and can reach the
51+
run's identity, partial state, and artifacts meanwhile;
52+
:meth:`~nemo_evaluator_sdk.execution.evaluator.Evaluator.submit` waits on the caller's
53+
behalf. A backend that runs in-process returns a
54+
:class:`~nemo_evaluator_sdk.execution.jobs.LocalJob`, which likewise defers the work to the
55+
wait, so the call means the same thing wherever it executed. Implementations may accept extra keyword arguments with defaults (a
56+
workspace, a metric packager) without breaking conformance.
4457
4558
Args:
46-
metric: Metric to prepare and execute.
47-
dataset: Inline dataset rows, a dataset file, or a dataset directory/glob path.
48-
params: Validated run configuration for the selected target mode.
49-
target: Optional model or agent used to generate candidate responses before scoring.
50-
field_mapping: Optional mapping from canonical evaluator fields to dataset columns.
51-
prompt_template: Optional prompt template for online target generation.
52-
aggregate_fields: Optional aggregate score fields to keep in the returned result.
53-
preprocess_hooks: Optional request preprocess hooks for online execution.
54-
postprocess_hooks: Optional response postprocess hooks for online execution.
59+
taskset: Tasks to evaluate, each carrying its own metrics.
60+
target: What generates trials — a model, agent, or runner. Mutually exclusive
61+
with ``trials``.
62+
trials: Precomputed trials to score instead of generating them. Mutually exclusive
63+
with ``target``.
64+
config: Run-level execution settings.
5565
5666
Returns:
57-
The completed single-metric evaluation result.
67+
The job, awaited through its own methods.
5868
"""
5969
...
6070

61-
async def evaluate_benchmark(
71+
async def evaluate_dataset(
6272
self,
6373
*,
6474
metrics: Sequence[Metric],
@@ -71,7 +81,7 @@ async def evaluate_benchmark(
7181
preprocess_hooks: tuple[PreprocessRequest, ...] | None = None,
7282
postprocess_hooks: tuple[PostprocessResponse, ...] | None = None,
7383
) -> BenchmarkEvaluationResult:
74-
"""Evaluate multiple metrics directly and return the completed result.
84+
"""Evaluate multiple metrics over a dataset and return the completed result.
7585
7686
Args:
7787
metrics: Metrics to prepare and execute together.
@@ -90,39 +100,41 @@ async def evaluate_benchmark(
90100
...
91101

92102

103+
@runtime_checkable
93104
class SyncEvaluationBackend(Protocol):
94105
def evaluate(
95106
self,
96107
*,
97-
metric: Metric,
98-
dataset: DatasetInput | str | Path,
99-
params: BackendParams,
100-
target: Model | Agent | None = None,
101-
field_mapping: FieldMapping | None = None,
102-
prompt_template: str | dict[str, Any] | None = None,
103-
aggregate_fields: tuple[AggregateFieldName, ...] | None = None,
104-
preprocess_hooks: tuple[PreprocessRequest, ...] | None = None,
105-
postprocess_hooks: tuple[PostprocessResponse, ...] | None = None,
106-
) -> EvaluationResult:
107-
"""Evaluate one metric directly and return the completed result.
108+
taskset: Sequence[AgentEvalTask],
109+
target: AgentEvalTarget | None = None,
110+
trials: Sequence[AgentEvalTrial] | None = None,
111+
config: AgentEvalRunConfig | None = None,
112+
) -> SyncEvaluationJob[AgentEvalResult]:
113+
"""Start evaluating a taskset — tasks that each carry their own metrics — and return its job.
114+
115+
The sync counterpart of :meth:`EvaluationBackend.evaluate`.
116+
117+
The entrypoint ``evaluate_dataset`` is intended to fold into: a dataset with one shared
118+
metric list is a taskset whose metrics have been hoisted. That is not implemented yet — this
119+
method cannot express a dataset today — so ``evaluate_dataset`` remains the way to run one,
120+
and is not deprecated.
121+
122+
Returns a job rather than a result; see :meth:`EvaluationBackend.evaluate`.
108123
109124
Args:
110-
metric: Metric to prepare and execute.
111-
dataset: Inline dataset rows, a dataset file, or a dataset directory/glob path.
112-
params: Validated run configuration for the selected target mode.
113-
target: Optional model or agent used to generate candidate responses before scoring.
114-
field_mapping: Optional mapping from canonical evaluator fields to dataset columns.
115-
prompt_template: Optional prompt template for online target generation.
116-
aggregate_fields: Optional aggregate score fields to keep in the returned result.
117-
preprocess_hooks: Optional request preprocess hooks for online execution.
118-
postprocess_hooks: Optional response postprocess hooks for online execution.
125+
taskset: Tasks to evaluate, each carrying its own metrics.
126+
target: What generates trials — a model, agent, or runner. Mutually exclusive
127+
with ``trials``.
128+
trials: Precomputed trials to score instead of generating them. Mutually exclusive
129+
with ``target``.
130+
config: Run-level execution settings.
119131
120132
Returns:
121-
The completed single-metric evaluation result.
133+
The job, awaited through its own methods.
122134
"""
123135
...
124136

125-
def evaluate_benchmark(
137+
def evaluate_dataset(
126138
self,
127139
*,
128140
metrics: Sequence[Metric],
@@ -135,7 +147,7 @@ def evaluate_benchmark(
135147
preprocess_hooks: tuple[PreprocessRequest, ...] | None = None,
136148
postprocess_hooks: tuple[PostprocessResponse, ...] | None = None,
137149
) -> BenchmarkEvaluationResult:
138-
"""Evaluate multiple metrics directly and return the completed result.
150+
"""Evaluate multiple metrics over a dataset and return the completed result.
139151
140152
Args:
141153
metrics: Metrics to prepare and execute together.

0 commit comments

Comments
 (0)