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.
- gooddata_eval/cli/agentic_runner.py +38 -0
- gooddata_eval/core/agentic/_failed_runs.py +62 -0
- gooddata_eval/core/agentic/alert_skill.py +42 -18
- gooddata_eval/core/agentic/anomaly_detection.py +10 -0
- gooddata_eval/core/agentic/dashboard_skill.py +589 -37
- gooddata_eval/core/agentic/general_question.py +70 -22
- gooddata_eval/core/agentic/guardrail.py +67 -18
- gooddata_eval/core/agentic/kda_skill.py +38 -14
- gooddata_eval/core/agentic/metric_skill.py +34 -12
- gooddata_eval/core/agentic/report_skill.py +22 -7
- gooddata_eval/core/agentic/search_tool.py +28 -6
- gooddata_eval/core/agentic/visualization.py +34 -9
- gooddata_eval/core/agentic/what_if.py +26 -2
- gooddata_eval/core/evaluators/_llm_judge.py +12 -0
- gooddata_eval/core/models.py +7 -0
- gooddata_eval/core/reporting/html_report.py +27 -1
- gooddata_eval/core/reporting/json_report.py +6 -0
- gooddata_eval/core/runner.py +40 -0
- {gooddata_eval-1.76.1.dev3.dist-info → gooddata_eval-1.76.1.dev4.dist-info}/METADATA +2 -2
- {gooddata_eval-1.76.1.dev3.dist-info → gooddata_eval-1.76.1.dev4.dist-info}/RECORD +23 -22
- {gooddata_eval-1.76.1.dev3.dist-info → gooddata_eval-1.76.1.dev4.dist-info}/WHEEL +0 -0
- {gooddata_eval-1.76.1.dev3.dist-info → gooddata_eval-1.76.1.dev4.dist-info}/entry_points.txt +0 -0
- {gooddata_eval-1.76.1.dev3.dist-info → gooddata_eval-1.76.1.dev4.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
)
|