Skip to content

Commit 3098e8e

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 3098e8e

51 files changed

Lines changed: 3235 additions & 2104 deletions

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/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
from nemo_evaluator_sdk.datasets import DatasetLoadError, load_dataset, load_dataset_as_dicts
3434
from nemo_evaluator_sdk.execution.backends.local.backend import LocalBackend
3535
from nemo_evaluator_sdk.execution.evaluator import Evaluator
36+
from nemo_evaluator_sdk.execution.jobs import (
37+
EvaluationJob,
38+
LocalJob,
39+
SyncEvaluationJob,
40+
)
3641
from nemo_evaluator_sdk.execution.values import (
3742
EvaluationError,
3843
EvaluationPhase,
@@ -116,6 +121,9 @@
116121
"load_dataset_as_dicts": ".datasets",
117122
"LocalBackend": ".execution.backends.local.backend",
118123
"Evaluator": ".execution.evaluator",
124+
"EvaluationJob": ".execution.jobs",
125+
"SyncEvaluationJob": ".execution.jobs",
126+
"LocalJob": ".execution.jobs",
119127
"EvaluationError": ".execution.values",
120128
"EvaluationPhase": ".execution.values",
121129
"BLEUMetric": ".metrics.bleu",
@@ -189,6 +197,9 @@
189197
"RunConfigOnlineModel",
190198
"EvaluationResult",
191199
"Evaluator",
200+
"EvaluationJob",
201+
"SyncEvaluationJob",
202+
"LocalJob",
192203
"ExactMatchMetric",
193204
"F1Metric",
194205
"FieldMapping",

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

Lines changed: 73 additions & 5 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
@@ -140,6 +162,24 @@ def __init__(
140162
self.client = client
141163
self.default_headers = default_headers
142164

165+
@overload
166+
async def run(
167+
self,
168+
*,
169+
tasks: Sequence[AgentEvalTask],
170+
target: AgentEvalTarget,
171+
config: AgentEvalRunConfig | None = None,
172+
) -> AgentEvalResult: ...
173+
174+
@overload
175+
async def run(
176+
self,
177+
*,
178+
tasks: Sequence[AgentEvalTask],
179+
trials: Sequence[AgentEvalTrial],
180+
config: AgentEvalRunConfig | None = None,
181+
) -> AgentEvalResult: ...
182+
143183
async def run(
144184
self,
145185
*,
@@ -150,12 +190,13 @@ async def run(
150190
) -> AgentEvalResult:
151191
"""Evaluate imported trials or generate live trials before scoring.
152192
153-
Exactly one of ``trials`` or ``target`` must be provided.
193+
Exactly one of ``trials`` or ``target`` must be provided; the overloads above say so to the
194+
type checker, and :func:`validate_run_inputs` still says it at runtime for callers that
195+
assemble their arguments dynamically.
154196
"""
155197
resolved_config = config or AgentEvalRunConfig()
156198
task_list = list(tasks)
157-
if not task_list:
158-
raise ValueError("at least one task is required")
199+
validate_run_inputs(tasks=task_list, trials=trials, target=target)
159200

160201
run_id = resolved_config.run_id or _new_run_id()
161202
runtime_config = resolved_config.model_copy(update={"run_id": run_id})
@@ -199,6 +240,24 @@ async def run(
199240

200241
return result
201242

243+
@overload
244+
def run_sync(
245+
self,
246+
*,
247+
tasks: Sequence[AgentEvalTask],
248+
target: AgentEvalTarget,
249+
config: AgentEvalRunConfig | None = None,
250+
) -> AgentEvalResult: ...
251+
252+
@overload
253+
def run_sync(
254+
self,
255+
*,
256+
tasks: Sequence[AgentEvalTask],
257+
trials: Sequence[AgentEvalTrial],
258+
config: AgentEvalRunConfig | None = None,
259+
) -> AgentEvalResult: ...
260+
202261
def run_sync(
203262
self,
204263
*,
@@ -207,8 +266,17 @@ def run_sync(
207266
target: AgentEvalTarget | None = None,
208267
config: AgentEvalRunConfig | None = None,
209268
) -> AgentEvalResult:
210-
"""Synchronous bridge for :meth:`run`."""
211-
return run_sync(lambda: self.run(tasks=tasks, trials=trials, target=target, config=config))
269+
"""Synchronous bridge for :meth:`run`.
270+
271+
Branches on which seam was supplied because the overloads keep the two apart; the final
272+
raise is what narrows, and is unreachable once one of them is set.
273+
"""
274+
validate_run_inputs(tasks=tasks, trials=trials, target=target)
275+
if trials is not None:
276+
return run_sync(lambda: self.run(tasks=tasks, trials=trials, config=config))
277+
if target is not None:
278+
return run_sync(lambda: self.run(tasks=tasks, target=target, config=config))
279+
raise ValueError("provide exactly one of trials or target")
212280

213281
async def _score_trials(
214282
self,

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
```

0 commit comments

Comments
 (0)