[NOTICKET] test: planner eval cases for trend-chart aggregation + column disambiguation
Browse filesExtends eval/planner to guard the two planner bugs the trend-chart fix addresses:
- New `chart` cases: a trend chart must be aggregated (assertion `chart_aggregated`
= group_by in the IR OR any analyze_* step), not fed raw rows to render_chart.
- New `column_disambiguation` cases: "trend PA" must select PA_Percent, NOT
Plan_PA_Percent — checked by column_id (assertions `selects_col`/`not_selects_col`)
so a wrong column hidden behind alias "pa_percent" is still caught.
New assertions: chart_aggregated, selects_col, not_selects_col, any_tool. Dataset
27 -> 31 cases. Selfcheck extended to cover the new scorer paths (raw->chart fails;
group_by->chart and analyze_trend->chart pass; wrong-aliased-column fails).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- eval/planner/README.md +2 -0
- eval/planner/planner_dataset.json +36 -0
- eval/planner/run_eval.py +51 -1
|
@@ -31,6 +31,8 @@ Each run writes `results/planner_result_<timestamp>.json` (never overwritten).
|
|
| 31 |
| `count` | scalar count → `count` aggregate (shipped fix) |
|
| 32 |
| `ranking` | top/bottom-N entities → `group_by` + `avg` + `order_by` + `limit` (**Bug 1**) |
|
| 33 |
| `fuzzy_filter` | partial model ref → `like`, never enumerate from samples (**Bug 2**) |
|
|
|
|
|
|
|
| 34 |
| `aggregate`, `descriptive`, `correlation`, `trend`, `merge` | believed-correct baselines |
|
| 35 |
| `counter_raw_rows` | "show N records" must stay raw rows (guards Bug 1 fix from over-aggregating) |
|
| 36 |
| `counter_exact_filter` | exact filters stay exact (guards Bug 2 fix from over-`like`ing) |
|
|
|
|
| 31 |
| `count` | scalar count → `count` aggregate (shipped fix) |
|
| 32 |
| `ranking` | top/bottom-N entities → `group_by` + `avg` + `order_by` + `limit` (**Bug 1**) |
|
| 33 |
| `fuzzy_filter` | partial model ref → `like`, never enumerate from samples (**Bug 2**) |
|
| 34 |
+
| `column_disambiguation` | "trend PA" must select `PA_Percent`, NOT `Plan_PA_Percent` (a wrong-column pick hidden behind alias `pa_percent`) |
|
| 35 |
+
| `chart` | a trend chart must aggregate before `render_chart` (not feed it 9,729 raw rows) + pick the right column |
|
| 36 |
| `aggregate`, `descriptive`, `correlation`, `trend`, `merge` | believed-correct baselines |
|
| 37 |
| `counter_raw_rows` | "show N records" must stay raw rows (guards Bug 1 fix from over-aggregating) |
|
| 38 |
| `counter_exact_filter` | exact filters stay exact (guards Bug 2 fix from over-`like`ing) |
|
|
@@ -3,8 +3,12 @@
|
|
| 3 |
"_expect_keys": {
|
| 4 |
"has_tool": "some task tool_call uses this tool name",
|
| 5 |
"no_tool": "no task uses this tool",
|
|
|
|
|
|
|
|
|
|
| 6 |
"select_agg": "some retrieve_data IR select has an agg with this fn (count/sum/avg/min/max/count_distinct)",
|
| 7 |
"group_by": "true = some IR has a non-empty group_by; false = NO IR has group_by (raw-row guard)",
|
|
|
|
| 8 |
"group_by_col": "some IR group_by contains this column NAME",
|
| 9 |
"filter_op": "some IR filter uses this op",
|
| 10 |
"no_filter_op": "NO IR filter uses this op",
|
|
@@ -214,6 +218,38 @@
|
|
| 214 |
"expect": {"select_agg": "count", "has_filter": true},
|
| 215 |
"carried_over": true
|
| 216 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
{
|
| 218 |
"id": "infeasible_churn_id",
|
| 219 |
"category": "infeasible",
|
|
|
|
| 3 |
"_expect_keys": {
|
| 4 |
"has_tool": "some task tool_call uses this tool name",
|
| 5 |
"no_tool": "no task uses this tool",
|
| 6 |
+
"any_tool": "at least one of these tool names is present (e.g. an aggregation step)",
|
| 7 |
+
"selects_col": "some retrieve_data select references this column NAME (by column_id, not the alias — catches wrong-column-hidden-behind-alias)",
|
| 8 |
+
"not_selects_col": "NO select references this column NAME (e.g. must not chart Plan_PA_Percent when the user asked for PA_Percent)",
|
| 9 |
"select_agg": "some retrieve_data IR select has an agg with this fn (count/sum/avg/min/max/count_distinct)",
|
| 10 |
"group_by": "true = some IR has a non-empty group_by; false = NO IR has group_by (raw-row guard)",
|
| 11 |
+
"chart_aggregated": "true = the chart's data is aggregated (group_by in the IR OR any analyze_* step present), not raw per-record rows",
|
| 12 |
"group_by_col": "some IR group_by contains this column NAME",
|
| 13 |
"filter_op": "some IR filter uses this op",
|
| 14 |
"no_filter_op": "NO IR filter uses this op",
|
|
|
|
| 218 |
"expect": {"select_agg": "count", "has_filter": true},
|
| 219 |
"carried_over": true
|
| 220 |
},
|
| 221 |
+
{
|
| 222 |
+
"id": "disambig_trend_pa_id",
|
| 223 |
+
"category": "column_disambiguation",
|
| 224 |
+
"lang": "id",
|
| 225 |
+
"question": "bagaimana trend PA?",
|
| 226 |
+
"expect": {"selects_col": "PA_Percent", "not_selects_col": "Plan_PA_Percent"},
|
| 227 |
+
"carried_over": false
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"id": "disambig_avg_pa_en",
|
| 231 |
+
"category": "column_disambiguation",
|
| 232 |
+
"lang": "en",
|
| 233 |
+
"question": "what is the average PA per section?",
|
| 234 |
+
"expect": {"selects_col": "PA_Percent", "not_selects_col": "Plan_PA_Percent"},
|
| 235 |
+
"carried_over": false
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"id": "chart_trend_pa_id",
|
| 239 |
+
"category": "chart",
|
| 240 |
+
"lang": "id",
|
| 241 |
+
"question": "bagaimana visualisasi trend PA?",
|
| 242 |
+
"expect": {"has_tool": "render_chart", "chart_aggregated": true, "selects_col": "PA_Percent", "not_selects_col": "Plan_PA_Percent"},
|
| 243 |
+
"carried_over": false
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"id": "chart_avg_pa_by_section_en",
|
| 247 |
+
"category": "chart",
|
| 248 |
+
"lang": "en",
|
| 249 |
+
"question": "show me a bar chart of average PA per section",
|
| 250 |
+
"expect": {"has_tool": "render_chart", "group_by": true, "selects_col": "PA_Percent"},
|
| 251 |
+
"carried_over": false
|
| 252 |
+
},
|
| 253 |
{
|
| 254 |
"id": "infeasible_churn_id",
|
| 255 |
"category": "infeasible",
|
|
@@ -127,6 +127,14 @@ def _filter_ops(f: dict) -> list[str]:
|
|
| 127 |
return [flt.get("op") for ir in f["irs"] for flt in ir.get("filters", []) if isinstance(flt, dict)]
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def evaluate_facts(f: dict, expect: dict, n2id: dict[str, str]) -> list[tuple[str, bool, str]]:
|
| 131 |
"""Return [(check, passed, detail)] for every assertion. Grouping/agg checks
|
| 132 |
honor BOTH the IR and the analyze_aggregate tool."""
|
|
@@ -139,12 +147,26 @@ def evaluate_facts(f: dict, expect: dict, n2id: dict[str, str]) -> list[tuple[st
|
|
| 139 |
res.append((f"has_tool={want}", want in tools, f"tools={sorted(tools)}"))
|
| 140 |
elif key == "no_tool":
|
| 141 |
res.append((f"no_tool={want}", want not in tools, f"tools={sorted(tools)}"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
elif key == "select_agg":
|
| 143 |
syn = _AGG_SYN.get(want, {want})
|
| 144 |
got = _all_agg_fns(f)
|
| 145 |
res.append((f"select_agg={want}", any(g in syn for g in got), f"aggs={got}"))
|
| 146 |
elif key == "group_by":
|
| 147 |
res.append(("group_by" if want else "no_group_by", grouped == want, f"grouped={grouped}"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
elif key == "group_by_col":
|
| 149 |
col_id = n2id.get(want, want)
|
| 150 |
a2id = _alias_to_id(f)
|
|
@@ -384,7 +406,35 @@ def _selfcheck() -> None:
|
|
| 384 |
assert ev(ok_ir, {"no_filter_op": "in"}), "non-enumerated filter should pass"
|
| 385 |
|
| 386 |
assert ev(NS(tasks=[], infeasible_reason="no churn data"), {"infeasible": True})
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
|
| 389 |
|
| 390 |
if __name__ == "__main__":
|
|
|
|
| 127 |
return [flt.get("op") for ir in f["irs"] for flt in ir.get("filters", []) if isinstance(flt, dict)]
|
| 128 |
|
| 129 |
|
| 130 |
+
def _selected_col_ids(f: dict) -> list[str]:
|
| 131 |
+
"""Every column_id referenced in any retrieve_data select (column or agg).
|
| 132 |
+
Used to catch the wrong-column bug where the planner selects Plan_PA_Percent
|
| 133 |
+
but aliases it 'pa_percent' — the alias hides it, the column_id doesn't."""
|
| 134 |
+
return [s["column_id"] for ir in f["irs"] for s in ir.get("select", [])
|
| 135 |
+
if isinstance(s, dict) and s.get("column_id")]
|
| 136 |
+
|
| 137 |
+
|
| 138 |
def evaluate_facts(f: dict, expect: dict, n2id: dict[str, str]) -> list[tuple[str, bool, str]]:
|
| 139 |
"""Return [(check, passed, detail)] for every assertion. Grouping/agg checks
|
| 140 |
honor BOTH the IR and the analyze_aggregate tool."""
|
|
|
|
| 147 |
res.append((f"has_tool={want}", want in tools, f"tools={sorted(tools)}"))
|
| 148 |
elif key == "no_tool":
|
| 149 |
res.append((f"no_tool={want}", want not in tools, f"tools={sorted(tools)}"))
|
| 150 |
+
elif key == "any_tool": # at least one of these tools present
|
| 151 |
+
res.append((f"any_tool={want}", any(t in tools for t in want), f"tools={sorted(tools)}"))
|
| 152 |
+
elif key == "selects_col": # a select references this column (by id, not the alias)
|
| 153 |
+
col_id = n2id.get(want, want)
|
| 154 |
+
ids = _selected_col_ids(f)
|
| 155 |
+
res.append((f"selects_col={want}", col_id in ids, f"selected={ids}"))
|
| 156 |
+
elif key == "not_selects_col": # this column must NOT be selected (wrong-column guard)
|
| 157 |
+
col_id = n2id.get(want, want)
|
| 158 |
+
ids = _selected_col_ids(f)
|
| 159 |
+
res.append((f"not_selects_col={want}", col_id not in ids, f"selected={ids}"))
|
| 160 |
elif key == "select_agg":
|
| 161 |
syn = _AGG_SYN.get(want, {want})
|
| 162 |
got = _all_agg_fns(f)
|
| 163 |
res.append((f"select_agg={want}", any(g in syn for g in got), f"aggs={got}"))
|
| 164 |
elif key == "group_by":
|
| 165 |
res.append(("group_by" if want else "no_group_by", grouped == want, f"grouped={grouped}"))
|
| 166 |
+
elif key == "chart_aggregated": # chart data is aggregated somehow (group_by IR OR any analyze_* step), not raw rows
|
| 167 |
+
analyze = [t for t in tools if t.startswith("analyze_")]
|
| 168 |
+
ok = grouped or bool(analyze)
|
| 169 |
+
res.append(("chart_aggregated" if want else "chart_raw", ok == want, f"grouped={grouped} analyze={analyze}"))
|
| 170 |
elif key == "group_by_col":
|
| 171 |
col_id = n2id.get(want, want)
|
| 172 |
a2id = _alias_to_id(f)
|
|
|
|
| 406 |
assert ev(ok_ir, {"no_filter_op": "in"}), "non-enumerated filter should pass"
|
| 407 |
|
| 408 |
assert ev(NS(tasks=[], infeasible_reason="no churn data"), {"infeasible": True})
|
| 409 |
+
|
| 410 |
+
# column disambiguation: selecting Plan_PA_Percent aliased "pa_percent" must FAIL
|
| 411 |
+
disambig = {"selects_col": "PA_Percent", "not_selects_col": "Plan_PA_Percent"}
|
| 412 |
+
wrong_col = plan([[("retrieve_data", {"ir": {"select": [
|
| 413 |
+
{"kind": "column", "column_id": "c_plan_pa_percent", "alias": "pa_percent"}]}})]])
|
| 414 |
+
assert not ev(wrong_col, disambig), "wrong column (Plan_PA_Percent aliased pa_percent) should fail"
|
| 415 |
+
right_col = plan([[("retrieve_data", {"ir": {"select": [
|
| 416 |
+
{"kind": "column", "column_id": "c_pa_percent", "alias": "pa_percent"}]}})]])
|
| 417 |
+
assert ev(right_col, disambig), "right column (PA_Percent) should pass"
|
| 418 |
+
|
| 419 |
+
# trend chart must be aggregated (group_by in IR OR an analyze_* step), not raw
|
| 420 |
+
chart_exp = {"has_tool": "render_chart", "chart_aggregated": True}
|
| 421 |
+
raw_chart = plan([
|
| 422 |
+
[("retrieve_data", {"ir": {"select": [{"kind": "column", "column_id": "c_pa_percent"}]}})],
|
| 423 |
+
[("render_chart", {})]])
|
| 424 |
+
assert not ev(raw_chart, chart_exp), "raw retrieve -> chart (no aggregation) should fail"
|
| 425 |
+
# aggregate in the retrieve IR (group_by date) -> chart : valid, no analyze_* needed
|
| 426 |
+
grp_chart = plan([
|
| 427 |
+
[("retrieve_data", {"ir": {"select": [{"kind": "agg", "fn": "avg", "column_id": "c_pa_percent"}],
|
| 428 |
+
"group_by": ["c_from_date"]}})],
|
| 429 |
+
[("render_chart", {})]])
|
| 430 |
+
assert ev(grp_chart, chart_exp), "retrieve(group_by date) -> chart should pass"
|
| 431 |
+
# or analyze_trend -> chart : also valid
|
| 432 |
+
trend_chart = plan([
|
| 433 |
+
[("retrieve_data", {"ir": {"select": [{"kind": "column", "column_id": "c_pa_percent"}]}})],
|
| 434 |
+
[("analyze_trend", {})], [("render_chart", {})]])
|
| 435 |
+
assert ev(trend_chart, chart_exp), "retrieve -> trend -> chart should pass"
|
| 436 |
+
|
| 437 |
+
print("selfcheck OK — scorer distinguishes good vs buggy plans (agg paths + column + chart)")
|
| 438 |
|
| 439 |
|
| 440 |
if __name__ == "__main__":
|