Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from gooddata_eval.core.agentic.what_if import evaluate_agentic_what_if
from gooddata_eval.core.config import ReasoningEffort
from gooddata_eval.core.evaluators._llm_judge import JudgeResponseError
from gooddata_eval.core.evaluators.visualization import requires_execution_of
from gooddata_eval.core.evaluators.visualization import expectation_options
from gooddata_eval.core.models import AgenticEvalOutcome, CreatedVisualization, DatasetItem
from gooddata_eval.core.runner import EvalReport, ItemReport
from gooddata_eval.core.timing import TraceRecorder
Expand Down Expand Up @@ -119,6 +119,8 @@ def _parse_visualization_expected(expected_output: Any) -> list[CreatedVisualiza
{"visualization": {...}} or {"visualization": [{...}]} <- single/multi candidate
[{"visualization": {...}}, ...] <- bare list
"""
if isinstance(expected_output, dict) and expected_output.get("expects_visualization") is False:
return []
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if isinstance(expected_output, dict):
raw_list = expected_output.get("expected_outputs")
if raw_list is not None:
Expand Down Expand Up @@ -187,7 +189,7 @@ def _dispatch_agentic(
workspace_id=workspace_id,
question=item.question,
expected_outputs=_parse_visualization_expected(eo),
requires_execution=requires_execution_of(eo),
options=expectation_options(eo),
k=k,
gate=gate,
agent_id=agent_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
build_latency_breakdown,
shift_and_index_events,
)
from gooddata_eval.core.scoring import AAC_VISUALIZATION_TYPES, same_viz_type
from gooddata_eval.core.timing import PhaseTimings, log_timer, sum_timings

_DEFAULT_K = 1
Expand Down Expand Up @@ -582,6 +583,64 @@ def _check_date_bindings(widgets: list[dict], new_ids: set[str], expected: list[
return failures


def _has_chart_types(expected_output: dict) -> bool:
"""Whether any expected chart states the type it must be placed as."""
return any(isinstance(v, dict) and "chart_type" in v for v in expected_output.get("visualizations") or [])


def _bodies_by_id(*references: dict) -> dict[str, dict]:
"""Every chart body the response's references carry, existing and authored, by chart id."""
bodies: dict[str, dict] = {}
for refs in references:
for key in ("visualizations", "new_visualizations"):
for body in refs.get(key) or []:
if isinstance(body, dict) and body.get("id"):
bodies[body["id"]] = body
return bodies


def _check_chart_types(
widgets: list[dict], new_ids: set[str], expected: list[dict], bodies: dict[str, dict]
) -> tuple[list[str], list[str]]:
"""Every widget of a chart that states a ``chart_type`` must show a chart of exactly that type.

A widget names only the chart id, so the type is read from the chart body the references
carry. The title alone cannot catch a chart authored as another type than the one asked
for, which is the silent swap GDAI-2054 rules out.

Returns the failures and the notes. A widget whose chart has no body in the references
cannot be checked: gen-ai carries a body only when it could resolve one, so a missing
body is a platform fault, noted as ``_check_references`` notes a missing id, and not
scored as the agent's.
"""
failures: list[str] = []
notes: list[str] = []
for exp in expected:
if not isinstance(exp, dict) or "chart_type" not in exp:
continue
wanted = exp["chart_type"]
label = exp.get("title") if exp.get("id") is None else exp.get("id")
candidates = _matching_widgets(widgets, new_ids, exp)
if not candidates:
failures.append(f"cannot check the type of chart {label!r}: it is not on the dashboard")
continue
types: list[str] = []
for widget in candidates:
chart_type = (bodies.get(widget.get("visualization")) or {}).get("type")
if isinstance(chart_type, str):
types.append(chart_type)
else:
notes.append(
f"cannot check the type of chart {label!r}: the response's references carry no body "
f"for {widget.get('visualization')!r}"
)
# Every placement, as for date bindings: one right placement must not hide a wrong one.
if not all(same_viz_type(wanted, t) for t in types):
found = ", ".join(repr(t) for t in types)
failures.append(f"chart {label!r} is {found}, expected {wanted!r}")
return failures, notes


def _tabs_of(dashboard: dict) -> list[dict]:
"""The dashboard's tabs, reading a document with root sections as its single tab."""
tabs = dashboard.get("tabs")
Expand Down Expand Up @@ -936,6 +995,8 @@ def _validate_layout_expectations(expected_output: dict) -> None:
for chart in expected_output.get("visualizations") or []:
if isinstance(chart, dict) and "date_dataset" in chart and not _is_dataset_or_null(chart["date_dataset"]):
raise ValueError(f"date_dataset must name a date dataset or be null, got {chart!r}")
if isinstance(chart, dict) and "chart_type" in chart and chart["chart_type"] not in AAC_VISUALIZATION_TYPES:
raise ValueError(f"chart_type must be an AAC visualization type, got {chart!r}")

if "tabs" in expected_output:
tabs = expected_output["tabs"]
Expand Down Expand Up @@ -1037,6 +1098,7 @@ class _Applies:
tab_filters: bool = False
date_filters: bool = False
preserved_widgets: bool = False
chart_types: bool = False

@classmethod
def of(cls, expected_output: dict) -> _Applies:
Expand All @@ -1049,6 +1111,7 @@ def of(cls, expected_output: dict) -> _Applies:
tab_filters="tab_filters" in expected_output,
date_filters="date_filters" in expected_output,
preserved_widgets="preserved_widgets" in expected_output,
chart_types=_has_chart_types(expected_output),
)


Expand Down Expand Up @@ -1089,6 +1152,7 @@ class DashboardEvaluation:
tab_filters_correct: bool = False
date_filters_correct: bool = False
widgets_preserved: bool = False
chart_types_correct: bool = False
failures: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)

Expand Down Expand Up @@ -1129,6 +1193,8 @@ def strict_checks(self) -> dict[str, bool]:
checks["dashboard_date_filters_correct"] = self.date_filters_correct
if self.applies.preserved_widgets:
checks["dashboard_widgets_preserved"] = self.widgets_preserved
if self.applies.chart_types:
checks["dashboard_chart_types_correct"] = self.chart_types_correct
return checks

@property
Expand Down Expand Up @@ -1280,12 +1346,14 @@ def evaluate_dashboard_response(
| {v.get("id") for v in patch_references.get("visualizations") or [] if v.get("id")}
| new_ids
)
bodies = _bodies_by_id(base_references, patch_references)
else:
if actual_saved_id is not None:
saved_failures.append(f"a new draft must not be saved yet, but it reports id {actual_saved_id!r}")
document = base
new_ids = {v.get("id") for v in base_references.get("new_visualizations") or [] if v.get("id")}
known_ids = {v.get("id") for v in base_references.get("visualizations") or [] if v.get("id")} | new_ids
bodies = _bodies_by_id(base_references)

if document is None:
return DashboardEvaluation(
Expand Down Expand Up @@ -1337,6 +1405,11 @@ def evaluate_dashboard_response(
preserved_failures = (
_check_preserved_widgets(widgets, expected_output["preserved_widgets"]) if applies.preserved_widgets else []
)
chart_type_failures, chart_type_notes = (
_check_chart_types(widgets, new_ids, expected_output.get("visualizations") or [], bodies)
if applies.chart_types
else ([], [])
)

return DashboardEvaluation(
drafted=True,
Expand All @@ -1356,6 +1429,7 @@ def evaluate_dashboard_response(
tab_filters_correct=not tab_filter_failures,
date_filters_correct=not date_filter_failures,
widgets_preserved=not preserved_failures,
chart_types_correct=not chart_type_failures,
failures=[
*chart_failures,
*saved_failures,
Expand All @@ -1366,9 +1440,10 @@ def evaluate_dashboard_response(
*tab_filter_failures,
*date_filter_failures,
*preserved_failures,
*chart_type_failures,
*new_failures,
],
notes=[*title_notes, *reference_notes],
notes=[*title_notes, *reference_notes, *chart_type_notes],
)


Expand Down
Loading
Loading