Skip to content

Commit 1dcacb6

Browse files
authored
Merge pull request #1743 from gooddata/feat/eval-visualization-filter-detail
feat(gooddata-eval): report compared filters in visualization evaluation detail
2 parents f26babd + b946807 commit 1dcacb6

5 files changed

Lines changed: 129 additions & 0 deletions

File tree

‎packages/gooddata-eval/src/gooddata_eval/core/agentic/visualization.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ class VisualizationAssertionError(AssertionError):
254254
__tracebackhide__ = True
255255

256256

257+
def _filter_diff(category: str, ev: EvaluationResult) -> str:
258+
"""Expected-vs-actual lines for one filter category, or "" when they matched.
259+
260+
Filters compare by exact equality on their canonical form, so "False" alone leaves
261+
the reader guessing which part differed — the period, the granularity or the dataset.
262+
"""
263+
expected, actual = ev.expected_filters.get(category, []), ev.actual_filters.get(category, [])
264+
if expected == actual:
265+
return ""
266+
return f" expected : {expected or 'none'}\n actual : {actual or 'none'}\n"
267+
268+
257269
def evaluate_agentic_visualization(
258270
host: str,
259271
token: str,
@@ -393,8 +405,11 @@ def evaluate_agentic_visualization(
393405
f" actual : {sorted(ev.actual_dim_uris)}\n"
394406
f" Filters Correct : {ev.filters_correct}\n"
395407
f" date : {ev.filter_date_score}\n"
408+
f"{_filter_diff('date', ev)}"
396409
f" ranking : {ev.filter_ranking_score}\n"
410+
f"{_filter_diff('ranking', ev)}"
397411
f" attribute : {ev.filter_attribute_score}\n"
412+
f"{_filter_diff('attribute', ev)}"
398413
f" Viz Type Hard : {ev.viz_type_hard}\n"
399414
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
400415
)

‎packages/gooddata-eval/src/gooddata_eval/core/evaluators/visualization.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
check_viz_type,
1111
get_dimension_uri_set,
1212
get_metric_uri_set,
13+
normalized_filters,
1314
validate_cross_references,
1415
)
1516

17+
_NO_FILTERS: dict[str, list[str]] = {"date": [], "ranking": [], "attribute": []}
18+
1619

1720
@dataclass
1821
class EvaluationResult:
@@ -31,6 +34,11 @@ class EvaluationResult:
3134
actual_metric_uris: set[str]
3235
expected_dim_uris: set[str]
3336
actual_dim_uris: set[str]
37+
# Filters in the canonical form equality is tested on, keyed by the category each
38+
# `filter_*_score` covers. Reported alongside the booleans because a filter mismatch
39+
# is otherwise undiagnosable from a finished run.
40+
expected_filters: dict[str, list[str]]
41+
actual_filters: dict[str, list[str]]
3442

3543
@property
3644
def strict_pass(self) -> bool:
@@ -91,6 +99,8 @@ def _evaluate_visualization(
9199
actual_metric_uris=set(),
92100
expected_dim_uris=exp_dim_uris,
93101
actual_dim_uris=set(),
102+
expected_filters=normalized_filters(expected),
103+
actual_filters={category: values.copy() for category, values in _NO_FILTERS.items()},
94104
)
95105
cross_ref_valid, cross_ref_errors = validate_cross_references(actual)
96106
act_metric_uris = get_metric_uri_set(actual)
@@ -112,6 +122,8 @@ def _evaluate_visualization(
112122
actual_metric_uris=act_metric_uris,
113123
expected_dim_uris=exp_dim_uris,
114124
actual_dim_uris=act_dim_uris,
125+
expected_filters=normalized_filters(expected),
126+
actual_filters=normalized_filters(actual),
115127
)
116128

117129

@@ -174,5 +186,7 @@ def evaluate(self, item: DatasetItem, chat_result: ChatResult) -> ItemEvaluation
174186
"actual_metric_uris": sorted(ev.actual_metric_uris),
175187
"expected_dim_uris": sorted(ev.expected_dim_uris),
176188
"actual_dim_uris": sorted(ev.actual_dim_uris),
189+
"expected_filters": ev.expected_filters,
190+
"actual_filters": ev.actual_filters,
177191
},
178192
)

‎packages/gooddata-eval/src/gooddata_eval/core/scoring.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ def _split_and_normalize_filters(viz: CreatedVisualization) -> tuple[set[str], s
188188
return date_set, ranking_set, attr_set
189189

190190

191+
def normalized_filters(viz: CreatedVisualization) -> dict[str, list[str]]:
192+
"""A visualization's filters exactly as `check_filters` compares them.
193+
194+
Grouped by the three categories it scores separately and sorted for stable output.
195+
Each entry is the canonical JSON string equality is tested on, so reporting the
196+
expected and actual side by side shows precisely why a category did not match —
197+
a `filter_date_score` of False otherwise gives no clue whether the period differed,
198+
the granularity did, or the dataset the filter hangs off did.
199+
"""
200+
date_set, ranking_set, attr_set = _split_and_normalize_filters(viz)
201+
return {"date": sorted(date_set), "ranking": sorted(ranking_set), "attribute": sorted(attr_set)}
202+
203+
191204
def check_filters(expected: CreatedVisualization, actual: CreatedVisualization) -> FilterScores:
192205
exp_date, exp_rank, exp_attr = _split_and_normalize_filters(expected)
193206
act_date, act_rank, act_attr = _split_and_normalize_filters(actual)

‎packages/gooddata-eval/tests/test_scoring.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
check_viz_type,
66
get_dimension_uri_set,
77
get_metric_uri_set,
8+
normalized_filters,
89
uri_to_display_name,
910
validate_cross_references,
1011
)
@@ -161,3 +162,46 @@ def test_validate_cross_references_accepts_omitted_attribute_but_flags_missing_u
161162
ok, errors = validate_cross_references(no_using)
162163
assert ok is False
163164
assert "is required" in errors[0]
165+
166+
167+
def test_normalized_filters_groups_by_scored_category():
168+
"""The three categories `check_filters` scores separately, in the form it compares."""
169+
viz = CreatedVisualization.model_validate(
170+
{
171+
"id": "x",
172+
"type": "bar_chart",
173+
"query": {
174+
"fields": {"m": {"using": "metric/spend"}, "d": {"using": "label/merchant.name"}},
175+
"filter_by": {
176+
"f0": {
177+
"type": "date_filter",
178+
"using": "dataset/date",
179+
"granularity": "MONTH",
180+
"from": -1,
181+
"to": -1,
182+
},
183+
"f1": {"type": "ranking_filter", "using": "m", "top": 5},
184+
"f2": {"type": "attribute_filter", "using": "label/region", "state": {"include": ["EMEA"]}},
185+
},
186+
},
187+
"metrics": ["m"],
188+
"view_by": ["d"],
189+
}
190+
)
191+
grouped = normalized_filters(viz)
192+
assert set(grouped) == {"date", "ranking", "attribute"}
193+
assert all(len(v) == 1 for v in grouped.values())
194+
# The ranking entry carries the substituted sole dimension, matching what equality sees.
195+
assert '"dim_uri": "label/merchant.name"' in grouped["ranking"][0]
196+
197+
198+
def test_normalized_filters_is_empty_per_category_when_unfiltered():
199+
viz = CreatedVisualization.model_validate(
200+
{
201+
"id": "x",
202+
"type": "headline",
203+
"query": {"fields": {"m": {"using": "metric/spend"}}, "filter_by": {}},
204+
"metrics": ["m"],
205+
}
206+
)
207+
assert normalized_filters(viz) == {"date": [], "ranking": [], "attribute": []}

‎packages/gooddata-eval/tests/test_visualization_evaluator.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,46 @@ def test_evaluator_passes_when_agent_omits_ranking_attribute_on_single_dim_viz()
127127
assert result.detail["filter_ranking_score"] is True
128128
assert result.detail["filters_correct"] is True
129129
assert result.passed is True
130+
131+
132+
def _dated(granularity: str, frm: int, to: int):
133+
return {
134+
"id": "x",
135+
"type": "column_chart",
136+
"query": {
137+
"fields": {"m_rev": {"using": "metric/revenue"}, "d_q": {"using": "label/date.quarter"}},
138+
"filter_by": {
139+
"f_date": {
140+
"type": "date_filter",
141+
"using": "dataset/date",
142+
"granularity": granularity,
143+
"from": frm,
144+
"to": to,
145+
}
146+
},
147+
},
148+
"metrics": ["m_rev"],
149+
"view_by": ["d_q"],
150+
}
151+
152+
153+
def test_detail_reports_the_filters_that_were_compared():
154+
"""A `filter_date_score` of False is undiagnosable from a finished run without them:
155+
two encodings of the same period compare unequal and the booleans don't say which."""
156+
ev = get_evaluator("visualization")
157+
result = ev.evaluate(_item(_dated("MONTH", -11, 0)), _chat_result_with(_dated("MONTH", -12, -1)))
158+
159+
assert result.detail["filter_date_score"] is False
160+
expected, actual = result.detail["expected_filters"], result.detail["actual_filters"]
161+
assert '"from": -11' in expected["date"][0]
162+
assert '"from": -12' in actual["date"][0]
163+
assert expected["ranking"] == actual["ranking"] == []
164+
assert expected["attribute"] == actual["attribute"] == []
165+
166+
167+
def test_detail_filters_are_empty_when_no_visualization_was_created():
168+
ev = get_evaluator("visualization")
169+
empty = ChatResult.model_validate({"textResponse": "what metric?", "toolCallEvents": []})
170+
result = ev.evaluate(_item(_dated("MONTH", -11, 0)), empty)
171+
assert result.detail["actual_filters"] == {"date": [], "ranking": [], "attribute": []}
172+
assert len(result.detail["expected_filters"]["date"]) == 1

0 commit comments

Comments
 (0)