Skip to content

Commit 65c6f81

Browse files
SandyChapmanclaudesvc-nvskills-signing
authored
refactor(evaluator)!: collapse the dataset entrypoints into evaluate_dataset (#1202)
* refactor(evaluator)!: collapse the dataset entrypoints into evaluate_dataset The backend contract 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 the two collapse into a single `evaluate_dataset(metrics=[...])`. `Evaluator.run`/`run_sync` lose their single-metric overloads — three each — leaving the three that discriminate on config and target, where the choice actually matters. Callers pass a list. `BenchmarkEvaluationResult` gains the `__str__` that `EvaluationResult` already had. Without it, collapsing to one result type would have quietly regressed `print(result)` to a pydantic dump for every caller. Two smaller things fall out of having one destination rather than two. The plugin's evaluate job no longer unwraps a one-metric list to hit the singular overload. And a backend's flavour is now decided by a single method, so the async/sync/mixed three-way collapses to two: a lone method is either a coroutine function or it is not, and there is no mixed client left to reject. This frees the name `evaluate` for the taskset entrypoint, which follows separately; nothing claims it here. BREAKING CHANGE: `EvaluationBackend.evaluate` and `evaluate_benchmark` are replaced by `evaluate_dataset`, which takes a metric list and returns a `BenchmarkEvaluationResult`. `Evaluator.run` and `run_sync` no longer accept a bare metric. Signed-off-by: Sandy Chapman <schapman@nvidia.com> * fix(evaluator): repair the callers and exports the dataset collapse left behind The collapse removed the single-metric overloads but left seven `run_sync` calls in the high-level walkthrough passing a bare metric. They fail as `AttributeError: 'tuple' object has no attribute 'output_spec'`, which says nothing about the real problem. The earlier sweep matched `metrics=[` and these pass named variables, so none of them matched. `BenchmarkEvaluationResult` is now what every dataset run returns, but it was only reachable from `nemo_evaluator_sdk.values.multi_metric_results` — callers could not name the type they are handed. It is exported from the package root and from `values` alongside `EvaluationResult`. The walkthrough's directory example passed `pattern=`, which no `Evaluator` method has ever accepted, on this branch or before it. The loader already handles a glob path when no pattern is given, so the example points `dataset` at `part-*.jsonl` and keeps excluding the file it is meant to exclude. The skill reference still promised a bare metric and one of two result types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Sandy Chapman <schapman@nvidia.com> * Attach NVSkills validation signatures Signed-off-by: nvskills-svc-account <svc-nvskills-signing@nvidia.com> --------- Signed-off-by: Sandy Chapman <schapman@nvidia.com> Signed-off-by: nvskills-svc-account <svc-nvskills-signing@nvidia.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: nvskills-svc-account <svc-nvskills-signing@nvidia.com>
1 parent f93a6e0 commit 65c6f81

35 files changed

Lines changed: 276 additions & 835 deletions

File tree

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: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"from IPython.display import Markdown, display\n",
4141
"from nemo_evaluator_sdk.execution.evaluator import Evaluator\n",
4242
"from nemo_evaluator_sdk.metrics.exact_match import ExactMatchMetric\n",
43-
"from nemo_evaluator_sdk.values import DatasetRows, EvaluationResult"
43+
"from nemo_evaluator_sdk.values import BenchmarkEvaluationResult, DatasetRows"
4444
]
4545
},
4646
{
@@ -55,7 +55,7 @@
5555
"- [Input mode 2: DatasetRows](#datasetrows)\n",
5656
"- [Input mode 3: pyarrow.Table](#pyarrow-table)\n",
5757
"- [Input mode 4: single file path](#file-path)\n",
58-
"- [Input mode 5: directory + pattern](#directory-pattern)\n",
58+
"- [Input mode 5: directory + glob](#directory-pattern)\n",
5959
"- [Result APIs showcase](#result-apis)\n"
6060
]
6161
},
@@ -91,7 +91,7 @@
9191
" display(pd.DataFrame.from_records(records))\n",
9292
"\n",
9393
"\n",
94-
"def summarize_run(name: str, result: EvaluationResult) -> dict[str, object]:\n",
94+
"def summarize_run(name: str, result: BenchmarkEvaluationResult) -> dict[str, object]:\n",
9595
" aggregate = result.to_records(view=\"aggregate\")\n",
9696
" exact = next((row for row in aggregate if row.get(\"name\") == \"exact_match\"), aggregate[0] if aggregate else {})\n",
9797
" return {\n",
@@ -122,13 +122,13 @@
122122
"\n",
123123
"evaluator = Evaluator()\n",
124124
"\n",
125-
"runs: dict[str, EvaluationResult] = {}\n",
125+
"runs: dict[str, BenchmarkEvaluationResult] = {}\n",
126126
"\n",
127127
"workspace_snapshot = [\n",
128128
" {\"path\": str(file_path), \"purpose\": \"Single-file input\"},\n",
129129
" {\"path\": str(dataset_dir / \"part-000.jsonl\"), \"purpose\": \"Sharded input part 1\"},\n",
130130
" {\"path\": str(dataset_dir / \"part-001.jsonl\"), \"purpose\": \"Sharded input part 2\"},\n",
131-
" {\"path\": str(dataset_dir / \"ignored.jsonl\"), \"purpose\": \"Intentionally excluded question/answer pattern\"},\n",
131+
" {\"path\": str(dataset_dir / \"ignored.jsonl\"), \"purpose\": \"Intentionally excluded from the glob\"},\n",
132132
"]\n",
133133
"\n",
134134
"display(Markdown(f\"**Workspace:** `{workspace}`\"))\n",
@@ -169,7 +169,7 @@
169169
"# ExactMatchMetric\n",
170170
"exact_metric = ExactMatchMetric(reference=\"{{ expected }}\", candidate=\"{{ prediction }}\")\n",
171171
"exact_result = evaluator.run_sync(\n",
172-
" metrics=exact_metric,\n",
172+
" metrics=[exact_metric],\n",
173173
" dataset=[\n",
174174
" {\"expected\": \"Paris\", \"prediction\": \"Paris\"},\n",
175175
" {\"expected\": \"Jupiter\", \"prediction\": \"Saturn\"},\n",
@@ -191,7 +191,7 @@
191191
"# F1Metric\n",
192192
"f1_metric = F1Metric(reference=\"{{ reference }}\", candidate=\"{{ prediction }}\")\n",
193193
"f1_result = evaluator.run_sync(\n",
194-
" metrics=f1_metric,\n",
194+
" metrics=[f1_metric],\n",
195195
" dataset=[\n",
196196
" {\"reference\": \"The Eiffel Tower is in Paris.\", \"prediction\": \"Eiffel Tower is in Paris\"},\n",
197197
" {\"reference\": \"Python is a programming language.\", \"prediction\": \"Python is a snake\"},\n",
@@ -210,7 +210,7 @@
210210
"# BLEUMetric\n",
211211
"bleu_metric = BLEUMetric(references=[\"{{ reference }}\"], candidate=\"{{ prediction }}\")\n",
212212
"bleu_result = evaluator.run_sync(\n",
213-
" metrics=bleu_metric,\n",
213+
" metrics=[bleu_metric],\n",
214214
" dataset=[\n",
215215
" {\"reference\": \"the cat is on the mat\", \"prediction\": \"the cat is on the mat\"},\n",
216216
" {\"reference\": \"a dog runs in the park\", \"prediction\": \"dog runs at park\"},\n",
@@ -234,7 +234,7 @@
234234
" epsilon=0.01,\n",
235235
")\n",
236236
"number_result = evaluator.run_sync(\n",
237-
" metrics=number_metric,\n",
237+
" metrics=[number_metric],\n",
238238
" dataset=[\n",
239239
" {\"expected_value\": \"3.1416\", \"model_value\": \"3.1410\"},\n",
240240
" {\"expected_value\": \"2.50\", \"model_value\": \"2.61\"},\n",
@@ -253,7 +253,7 @@
253253
"# ROUGEMetric\n",
254254
"rouge_metric = ROUGEMetric(reference=\"{{ reference_summary }}\", candidate=\"{{ model_summary }}\")\n",
255255
"rouge_result = evaluator.run_sync(\n",
256-
" metrics=rouge_metric,\n",
256+
" metrics=[rouge_metric],\n",
257257
" dataset=[\n",
258258
" {\n",
259259
" \"reference_summary\": \"The launch succeeded after two delays caused by weather.\",\n",
@@ -282,7 +282,7 @@
282282
" right_template=\"{{ required_phrase }}\",\n",
283283
")\n",
284284
"string_result = evaluator.run_sync(\n",
285-
" metrics=string_metric,\n",
285+
" metrics=[string_metric],\n",
286286
" dataset=[\n",
287287
" {\"answer_text\": \"The SLA is 99.9% uptime.\", \"required_phrase\": \"99.9%\"},\n",
288288
" {\"answer_text\": \"Support is available weekdays only.\", \"required_phrase\": \"24/7\"},\n",
@@ -307,7 +307,7 @@
307307
"\n",
308308
"tool_metric = ToolCallingMetric(reference=\"{{item.reference}}\")\n",
309309
"tool_result = evaluator.run_sync(\n",
310-
" metrics=tool_metric,\n",
310+
" metrics=[tool_metric],\n",
311311
" dataset=[\n",
312312
" {\n",
313313
" \"reference\": [{\"function\": {\"name\": \"sum\", \"arguments\": {\"x\": 1, \"y\": 2}}}],\n",
@@ -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",
@@ -448,9 +448,9 @@
448448
"source": [
449449
"<a id=\"directory-pattern\"></a>\n",
450450
"\n",
451-
"## Input Mode 5: Directory Path + `pattern`\n",
451+
"## Input Mode 5: Directory Path + Glob\n",
452452
"\n",
453-
"For sharded datasets, pass the directory and target only matching files.\n"
453+
"For sharded datasets, point `dataset` at a glob path to target only matching files.\n"
454454
]
455455
},
456456
{
@@ -460,9 +460,9 @@
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 / \"part-*.jsonl\")\n",
464464
"\n",
465-
"runs[\"Directory + pattern\"] = directory_result\n",
465+
"runs[\"Directory + glob\"] = directory_result\n",
466466
"\n",
467467
"directory_result.print_summary(max_rows=2)"
468468
]
@@ -475,7 +475,7 @@
475475
"\n",
476476
"## Result APIs\n",
477477
"\n",
478-
"All runs return `EvaluationResult` with the same helper surface.\n"
478+
"All runs return `BenchmarkEvaluationResult` with the same helper surface.\n"
479479
]
480480
},
481481
{
@@ -519,7 +519,7 @@
519519
"\n",
520520
"- `evaluator.run_sync(metrics=..., dataset=...)` gives one clean API across inline rows, Arrow data, and local files.\n",
521521
"- Templates (`reference`, `candidate`) handle the mapping between dataset columns and metric inputs.\n",
522-
"- `EvaluationResult` supports both human-friendly summaries and dataframe/table pipelines.\n",
522+
"- `BenchmarkEvaluationResult` supports both human-friendly summaries and dataframe/table pipelines.\n",
523523
"- The exact same workflow scales from local notebook demos to production data slices."
524524
]
525525
}

packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
BooleanValue,
7474
CandidateOutput,
7575
ContinuousScore,
76+
BenchmarkEvaluationResult,
7677
DatasetRow,
7778
DatasetRows,
7879
DiscreteScore,
@@ -166,6 +167,7 @@ def _resolve_version() -> str:
166167
"BooleanValue": ".values",
167168
"CandidateOutput": ".values",
168169
"ContinuousScore": ".values",
170+
"BenchmarkEvaluationResult": ".values",
169171
"DatasetRow": ".values",
170172
"DatasetRows": ".values",
171173
"DiscreteScore": ".values",
@@ -206,6 +208,7 @@ def _resolve_version() -> str:
206208
"RunConfig",
207209
"RunConfigOnline",
208210
"RunConfigOnlineModel",
211+
"BenchmarkEvaluationResult",
209212
"EvaluationResult",
210213
"Evaluator",
211214
"ExactMatchMetric",

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: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,13 @@
2121
RunConfigOnlineModel,
2222
)
2323
from nemo_evaluator_sdk.values.multi_metric_results import BenchmarkEvaluationResult
24-
from nemo_evaluator_sdk.values.results import AggregateFieldName, EvaluationResult
24+
from nemo_evaluator_sdk.values.results import AggregateFieldName
2525

2626
BackendParams = RunConfig | RunConfigOnline | RunConfigOnlineModel
2727

2828

2929
class EvaluationBackend(Protocol):
30-
async def evaluate(
31-
self,
32-
*,
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.
44-
45-
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.
55-
56-
Returns:
57-
The completed single-metric evaluation result.
58-
"""
59-
...
60-
61-
async def evaluate_benchmark(
30+
async def evaluate_dataset(
6231
self,
6332
*,
6433
metrics: Sequence[Metric],
@@ -91,38 +60,7 @@ async def evaluate_benchmark(
9160

9261

9362
class SyncEvaluationBackend(Protocol):
94-
def evaluate(
95-
self,
96-
*,
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-
109-
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.
119-
120-
Returns:
121-
The completed single-metric evaluation result.
122-
"""
123-
...
124-
125-
def evaluate_benchmark(
63+
def evaluate_dataset(
12664
self,
12765
*,
12866
metrics: Sequence[Metric],

0 commit comments

Comments
 (0)