gooddata-eval 1.76.1.dev3__py3-none-any.whl → 1.76.1.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ from gooddata_eval.core.agentic.search_tool import evaluate_agentic_search_tool
24
24
  from gooddata_eval.core.agentic.visualization import evaluate_agentic_visualization
25
25
  from gooddata_eval.core.agentic.what_if import evaluate_agentic_what_if
26
26
  from gooddata_eval.core.config import ReasoningEffort
27
+ from gooddata_eval.core.evaluators._llm_judge import JudgeResponseError
27
28
  from gooddata_eval.core.evaluators.visualization import requires_execution_of
28
29
  from gooddata_eval.core.models import AgenticEvalOutcome, CreatedVisualization, DatasetItem
29
30
  from gooddata_eval.core.runner import EvalReport, ItemReport
@@ -302,6 +303,7 @@ def _dispatch_agentic(
302
303
  question=item.question,
303
304
  expected_output=eo if isinstance(eo, dict) else {},
304
305
  k=k,
306
+ gate=gate,
305
307
  agent_id=agent_id,
306
308
  user_context=item.user_context,
307
309
  **lf_kw,
@@ -355,6 +357,20 @@ def _apply_run_counts(item_report: ItemReport, source: Any) -> None:
355
357
  item_report.runs_ungraded = unscored
356
358
 
357
359
 
360
+ def _apply_failed_runs(item_report: ItemReport, source: Any) -> None:
361
+ """Copy the per-run failure records off an outcome or a failure, if the kind built any.
362
+
363
+ Read the same way from both, like the run counts above: a partially passing item raises,
364
+ so an item's failing runs reach the report through the exception at least as often as
365
+ through the outcome. A kind that builds none keeps the empty default, which reads as
366
+ "not instrumented" rather than "nothing failed" -- ``runs_passed``/``runs`` already say
367
+ how many failed.
368
+ """
369
+ failed_runs = getattr(source, "failed_runs", None)
370
+ if failed_runs:
371
+ item_report.failed_runs = list(failed_runs)
372
+
373
+
358
374
  def _apply_timings(item_report: ItemReport, timings: Any) -> None:
359
375
  """Copy an outcome's phase breakdown onto the item report, if the kind recorded one.
360
376
 
@@ -454,6 +470,7 @@ def run_agentic_items(
454
470
  item_report.best_detail = detail or {}
455
471
  _apply_timings(item_report, getattr(outcome, "timings", None))
456
472
  _apply_run_counts(item_report, outcome)
473
+ _apply_failed_runs(item_report, outcome)
457
474
  except AssertionError as exc:
458
475
  item_report.gate_passed = False if gated else None
459
476
  item_report.runs = k
@@ -463,11 +480,32 @@ def run_agentic_items(
463
480
  item_report.best_detail = getattr(exc, "detail", None) or {}
464
481
  _apply_timings(item_report, getattr(exc, "timings", None))
465
482
  _apply_run_counts(item_report, exc)
483
+ _apply_failed_runs(item_report, exc)
466
484
  # Read off the counts, not off the gate: pass^K fails items where runs did pass,
467
485
  # and reporting those as pass_at_k False would contradict the Langfuse score of
468
486
  # the same name. Kinds that report no count read as 0, i.e. a clean failure.
469
487
  item_report.pass_at_k = item_report.runs_passed > 0
470
488
  print(f"[agentic] {item.id} FAIL: {exc}", flush=True)
489
+ except JudgeResponseError as exc:
490
+ # Errored, like the branch below -- there is no verdict for any run, so this is
491
+ # not K failures -- but NOT diagnostically empty. This is the one failure mode
492
+ # where the per-run records are most worth having: the judge broke, so what the
493
+ # agent actually said is still there to read, and the conversation ids are how
494
+ # anyone gets to it. Reported with the runs it really drove rather than 0.
495
+ item_report.error = f"{type(exc).__name__}: {exc}"
496
+ item_report.runs = getattr(exc, "runs_effective", None) or k
497
+ item_report.reasoning_steps = getattr(exc, "reasoning_steps", None) or []
498
+ item_report.conversation_id = getattr(exc, "conversation_id", None)
499
+ item_report.response_id = getattr(exc, "response_id", None)
500
+ item_report.best_detail = getattr(exc, "detail", None) or {}
501
+ _apply_timings(item_report, getattr(exc, "timings", None))
502
+ _apply_run_counts(item_report, exc)
503
+ _apply_failed_runs(item_report, exc)
504
+ # Every run ungraded, by definition of this error -- said explicitly rather than
505
+ # left to _apply_run_counts, whose source is detail["unscored_runs"] and which a
506
+ # kind that attaches no diagnostics would leave at 0.
507
+ item_report.runs_ungraded = item_report.runs
508
+ print(f"[agentic] {item.id} UNGRADED: {exc}", flush=True)
471
509
  except Exception as exc:
472
510
  item_report.error = f"{type(exc).__name__}: {exc}"
473
511
  item_report.runs = 0
@@ -0,0 +1,62 @@
1
+ # (C) 2026 GoodData Corporation. All rights reserved.
2
+ """Per-run failure records for the agentic evaluators.
3
+
4
+ ``core/runner.py`` records one of these for every failing run of a single-shot item. The
5
+ agentic kinds cannot reuse it: each of them drives its own K-loop inside the evaluator and
6
+ hands ``cli/agentic_runner`` a single aggregate per item, so by the time the runner sees
7
+ the result the individual attempts are already collapsed into ``best``. The runs themselves
8
+ are not lost -- every kind keeps its ``run_results`` list -- they simply never leave the
9
+ evaluator. This turns that list into the same records the single-shot path writes.
10
+
11
+ Keys mirror ``core.runner._failed_run_record`` so one consumer can read ``failed_runs`` from
12
+ either path without branching on test kind. ``tool_call_count``/``tool_names`` are the
13
+ addition: the agentic kinds capture tool calls per run, and an agent that produced a final
14
+ answer having made no tool call at all answered from the model's own knowledge rather than
15
+ from the workspace -- a distinction no other recorded field exposes.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Callable, Sequence
21
+ from typing import Any
22
+
23
+
24
+ def build_failed_runs(
25
+ run_results: Sequence[Any],
26
+ *,
27
+ passed: Callable[[Any], bool],
28
+ detail: Callable[[Any], dict],
29
+ ) -> list[dict]:
30
+ """One record per run that did not pass, in the order the runs happened.
31
+
32
+ ``passed`` and ``detail`` are supplied by the caller because neither is uniform across
33
+ kinds: guardrail reads ``run.passed`` while visualization reads
34
+ ``run.eval_result.strict_pass``, and each kind's diagnostic dict is its own. Everything
35
+ read here directly goes through ``getattr`` with a default, so a kind whose run result
36
+ lacks a field (no response id, no tool capture) records a null instead of raising.
37
+
38
+ A run the judge could not grade is recorded like any other non-passing run, with its
39
+ ``error`` set. That matches ``core/runner.py``, which also records ungraded runs; the
40
+ verdict-level accounting of ungraded runs stays with the caller's ``unscored_runs``.
41
+ """
42
+ records: list[dict] = []
43
+ for run_index, run in enumerate(run_results, start=1):
44
+ if passed(run):
45
+ continue
46
+ tool_calls = list(getattr(run, "tool_call_events", None) or [])
47
+ reasoning_steps = list(getattr(run, "reasoning_steps", None) or [])
48
+ records.append(
49
+ {
50
+ "run_index": run_index,
51
+ "passed": False,
52
+ "error": getattr(run, "judge_error", None),
53
+ "detail": detail(run),
54
+ "conversation_id": getattr(run, "conversation_id", None),
55
+ "response_id": getattr(run, "response_id", None),
56
+ "reasoning_step_count": len(reasoning_steps),
57
+ "reasoning_steps": reasoning_steps,
58
+ "tool_call_count": len(tool_calls),
59
+ "tool_names": [getattr(e, "function_name", None) for e in tool_calls],
60
+ }
61
+ )
62
+ return records
@@ -12,6 +12,7 @@ from typing import Any
12
12
  from gooddata_sdk import GoodDataSdk
13
13
 
14
14
  from gooddata_eval.core.agentic._catalog import AnomalyDetectionGranularity, CatalogMetricAlert
15
+ from gooddata_eval.core.agentic._failed_runs import build_failed_runs
15
16
  from gooddata_eval.core.agentic._gate import (
16
17
  DEFAULT_GATE,
17
18
  EvalGate,
@@ -846,6 +847,33 @@ class AlertSkillAssertionError(AgenticAssertionError):
846
847
  """Raised when an alert-skill evaluation fails."""
847
848
 
848
849
 
850
+ def _run_detail(run: AlertRunResult) -> dict:
851
+ """The diagnostic fields for ONE run, shared by the best run and every failing one.
852
+
853
+ Extracted so a failing run is described by exactly the same keys as the winning run --
854
+ for this kind that means the full per-field check breakdown and the arguments the run sent.
855
+ """
856
+ ev = run.eval
857
+ return {
858
+ "alert_created": ev.alert_created,
859
+ "operator_correct": ev.operator_correct,
860
+ "threshold_correct": ev.threshold_correct,
861
+ "trigger_correct": ev.trigger_correct,
862
+ "filters_correct": ev.filters_correct,
863
+ "metric_correct": ev.metric_correct,
864
+ "recipients_correct": ev.recipients_correct,
865
+ "attributes_correct": ev.attributes_correct,
866
+ "granularity_correct": ev.granularity_correct,
867
+ "actual_alert_arguments": run.actual_alert_arguments,
868
+ # Why the loop stopped. alert_created=False alone cannot tell a refusal from a run
869
+ # that hit max_iterations while still on track -- see LoopExit. Per-run rather than
870
+ # best-only: on a failing run it is the first thing worth reading.
871
+ "exit_reason": run.exit_reason.value,
872
+ "turns_used": run.turns_used,
873
+ **timeline_detail(run.tool_call_events, run.reasoning_step_events),
874
+ }
875
+
876
+
849
877
  def evaluate_agentic_alert_skill(
850
878
  host: str,
851
879
  token: str,
@@ -953,24 +981,16 @@ def evaluate_agentic_alert_skill(
953
981
 
954
982
  best = summary.best
955
983
  ev = best.eval
956
- detail = {
957
- "alert_created": ev.alert_created,
958
- "operator_correct": ev.operator_correct,
959
- "threshold_correct": ev.threshold_correct,
960
- "trigger_correct": ev.trigger_correct,
961
- "filters_correct": ev.filters_correct,
962
- "metric_correct": ev.metric_correct,
963
- "recipients_correct": ev.recipients_correct,
964
- "attributes_correct": ev.attributes_correct,
965
- "granularity_correct": ev.granularity_correct,
966
- "actual_alert_arguments": best.actual_alert_arguments,
967
- # Why the loop stopped. alert_created=False alone cannot tell a refusal from a run
968
- # that hit max_iterations while still on track -- see LoopExit.
969
- "exit_reason": best.exit_reason.value,
970
- "turns_used": best.turns_used,
971
- "max_iterations": max_iterations,
972
- **timeline_detail(best.tool_call_events, best.reasoning_step_events),
973
- }
984
+ # max_iterations is the same for every run, so it stays at the item level rather than
985
+ # being repeated into each failing run's detail.
986
+ detail = {**_run_detail(best), "max_iterations": max_iterations}
987
+ # Same predicate runs_passed is taken over, so an item's failed_runs and its counts
988
+ # cannot disagree about which runs failed.
989
+ failed_runs = build_failed_runs(
990
+ summary.run_results,
991
+ passed=lambda r: r.eval.strict_pass,
992
+ detail=_run_detail,
993
+ )
974
994
 
975
995
  if not gate_passed(gate, pass_at_k=summary.pass_at_k, pass_power_k=summary.pass_power_k):
976
996
  gate_note = gate_failure_note(gate, runs_passed, runs_effective)
@@ -990,6 +1010,7 @@ def evaluate_agentic_alert_skill(
990
1010
  exc.detail = detail
991
1011
  exc.runs_passed = runs_passed
992
1012
  exc.runs_effective = runs_effective
1013
+ exc.failed_runs = failed_runs
993
1014
  raise exc
994
1015
  return AgenticEvalOutcome(
995
1016
  runs_passed=runs_passed,
@@ -998,4 +1019,7 @@ def evaluate_agentic_alert_skill(
998
1019
  conversation_id=best.conversation_id,
999
1020
  response_id=best.response_id,
1000
1021
  detail=detail,
1022
+ # Also on the success path: pass@K clears the gate with one passing run, so a
1023
+ # 1/3 item reports success while two of its runs failed for reasons worth reading.
1024
+ failed_runs=failed_runs,
1001
1025
  )
@@ -31,6 +31,7 @@ import re
31
31
  from dataclasses import dataclass, field
32
32
  from typing import Any
33
33
 
34
+ from gooddata_eval.core.agentic._failed_runs import build_failed_runs
34
35
  from gooddata_eval.core.agentic._gate import (
35
36
  DEFAULT_GATE,
36
37
  EvalGate,
@@ -619,6 +620,13 @@ def evaluate_agentic_anomaly_detection(
619
620
  best = summary.best
620
621
  ev = best.evaluation
621
622
  detail = _detail(best)
623
+ # Same predicate runs_passed is taken over, so an item's failed_runs and its counts
624
+ # cannot disagree about which runs failed.
625
+ failed_runs = build_failed_runs(
626
+ summary.run_results,
627
+ passed=lambda r: r.evaluation.strict_pass,
628
+ detail=_detail,
629
+ )
622
630
  runs_passed = sum(1 for r in summary.run_results if r.evaluation.strict_pass)
623
631
 
624
632
  if not gate_passed(gate, pass_at_k=summary.pass_at_k, pass_power_k=summary.pass_power_k):
@@ -638,6 +646,7 @@ def evaluate_agentic_anomaly_detection(
638
646
  exc.detail = detail
639
647
  exc.runs_passed = runs_passed
640
648
  exc.runs_effective = len(summary.run_results)
649
+ exc.failed_runs = failed_runs
641
650
  raise exc
642
651
 
643
652
  return AgenticEvalOutcome(
@@ -647,4 +656,5 @@ def evaluate_agentic_anomaly_detection(
647
656
  conversation_id=best.conversation_id,
648
657
  response_id=best.response_id,
649
658
  detail=detail,
659
+ failed_runs=failed_runs,
650
660
  )