|
46 | 46 | make_agent_inference_fn, |
47 | 47 | new_agent_inference_client, |
48 | 48 | ) |
49 | | -from nemo_evaluator_sdk.execution.metric_execution import generate_online_sample, run_sync |
| 49 | +from nemo_evaluator_sdk.execution.metric_execution import ( |
| 50 | + generate_online_sample, |
| 51 | + resolve_target_structured_output_mode, |
| 52 | + run_sync, |
| 53 | +) |
| 54 | +from nemo_evaluator_sdk.structured_output import structured_output_mode_session |
50 | 55 | from nemo_evaluator_sdk.execution.samples import build_metric_input |
51 | 56 | from nemo_evaluator_sdk.inference import InferenceFn |
52 | | -from nemo_evaluator_sdk.metrics.protocol import Metric, validate_metric_result |
| 57 | +from nemo_evaluator_sdk.metrics.protocol import Metric, MetricWithPreflight, validate_metric_result |
53 | 58 | from nemo_evaluator_sdk.metrics.utils import metric_type_name |
54 | 59 | from nemo_evaluator_sdk.values import ( |
55 | 60 | Agent, |
@@ -161,22 +166,26 @@ async def run( |
161 | 166 | runtime_config = resolved_config.model_copy(update={"run_id": run_id}) |
162 | 167 | started_at = datetime.now(UTC) |
163 | 168 |
|
164 | | - # Branch on which seam was supplied so the type checker can narrow ``target`` to a |
165 | | - # concrete ``AgentEvalTarget`` without a cast. |
166 | | - if trials is not None: |
167 | | - if target is not None: |
| 169 | + # One detection session for the whole run: generation probes the target and scoring probes |
| 170 | + # any judge model, and imported-trial runs still score, so scoping this to generation alone |
| 171 | + # would leave judges probing per call. |
| 172 | + async with structured_output_mode_session(): |
| 173 | + # Branch on which seam was supplied so the type checker can narrow ``target`` to a |
| 174 | + # concrete ``AgentEvalTarget`` without a cast. |
| 175 | + if trials is not None: |
| 176 | + if target is not None: |
| 177 | + raise ValueError("provide exactly one of trials or target") |
| 178 | + trial_list = list(trials) |
| 179 | + elif target is not None: |
| 180 | + trial_list = await self._generate_trials(tasks=task_list, target=target, config=runtime_config) |
| 181 | + else: |
168 | 182 | raise ValueError("provide exactly one of trials or target") |
169 | | - trial_list = list(trials) |
170 | | - elif target is not None: |
171 | | - trial_list = await self._generate_trials(tasks=task_list, target=target, config=runtime_config) |
172 | | - else: |
173 | | - raise ValueError("provide exactly one of trials or target") |
174 | | - scores = await self._score_trials( |
175 | | - tasks=task_list, |
176 | | - trials=trial_list, |
177 | | - config=runtime_config, |
178 | | - run_id=run_id, |
179 | | - ) |
| 183 | + scores = await self._score_trials( |
| 184 | + tasks=task_list, |
| 185 | + trials=trial_list, |
| 186 | + config=runtime_config, |
| 187 | + run_id=run_id, |
| 188 | + ) |
180 | 189 | runner_scores = _collect_runner_aggregate_scores(target) if target is not None else [] |
181 | 190 | finished_at = datetime.now(UTC) |
182 | 191 | metadata = RunMetadata( |
@@ -237,6 +246,19 @@ async def _score_trials( |
237 | 246 | if not task.metrics: |
238 | 247 | raise ValueError(f"task {task.id!r} does not declare any metrics") |
239 | 248 |
|
| 249 | + # Agent-eval scores metrics directly rather than through prepare_metric_for_execution, so |
| 250 | + # nothing else runs their preflight. An LLM judge detects its endpoint's structured-output |
| 251 | + # encoding there; without this it would score using the provisional guess from new_hooks. |
| 252 | + # Deduplicated by identity because the same metric object is scored once per trial. The run |
| 253 | + # session would collapse repeat probes to one request anyway; this just avoids the repeated |
| 254 | + # awaits. Identity is stable here: `tasks` holds every metric for the duration of the loop. |
| 255 | + preflighted: set[int] = set() |
| 256 | + for task in tasks: |
| 257 | + for metric in task.metrics: |
| 258 | + if isinstance(metric, MetricWithPreflight) and id(metric) not in preflighted: |
| 259 | + preflighted.add(id(metric)) |
| 260 | + await metric.preflight() |
| 261 | + |
240 | 262 | semaphore = asyncio.Semaphore(config.parallelism) |
241 | 263 |
|
242 | 264 | async def guarded_score(task: AgentEvalTask, trial: AgentEvalTrial, metric: Metric) -> AgentEvalTaskScore: |
@@ -309,6 +331,8 @@ async def _generate_trials( |
309 | 331 | params = _resolve_live_params(config, target) |
310 | 332 | prompt_template = config.prompt_template or _default_prompt_template(target) |
311 | 333 | semaphore = asyncio.Semaphore(params.parallelism) |
| 334 | + # Hooks are built per row below; the run-level session opened by run() is what keeps the |
| 335 | + # endpoint probe to one round trip for the whole pass instead of one per row. |
312 | 336 |
|
313 | 337 | # Use the injected transport client when provided; otherwise build a default for the |
314 | 338 | # resolved target type and close it when generation finishes. |
@@ -393,10 +417,18 @@ async def _generate_sample( |
393 | 417 | # The transport client is a real class union, so isinstance narrowing is enough there. |
394 | 418 | if isinstance(target, Model): |
395 | 419 | model_params = cast(RunConfigOnlineModel, params) |
396 | | - preprocess_hooks, postprocess_hooks = inference.new_hooks(model_params, model_format=target.format) |
| 420 | + preprocess_hooks, postprocess_hooks = inference.new_hooks(model_params) |
397 | 421 | model_inference_fn = ( |
398 | 422 | cast(InferenceFn, inference_fn) if inference_fn is not None else inference.make_inference_request |
399 | 423 | ) |
| 424 | + # Hooks are built per row here, so this relies on detection being cached per endpoint: |
| 425 | + # without the probe the request would carry whichever encoding new_hooks guessed. |
| 426 | + await resolve_target_structured_output_mode( |
| 427 | + preprocess_hooks=preprocess_hooks, |
| 428 | + model=target, |
| 429 | + inference_fn=model_inference_fn, |
| 430 | + params=model_params, |
| 431 | + ) |
400 | 432 | return await generate_online_sample( |
401 | 433 | target=target, |
402 | 434 | row=row, |
|
0 commit comments