sofhiaazzhr Claude Opus 4.8 commited on
Commit
5edfc00
·
1 Parent(s): 0f3d8cc

[NOTICKET] test: planner eval cases for trend-chart aggregation + column disambiguation

Browse files

Extends 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 CHANGED
@@ -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) |
eval/planner/planner_dataset.json CHANGED
@@ -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",
eval/planner/run_eval.py CHANGED
@@ -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
- print("selfcheck OK — scorer distinguishes good vs buggy plans (IR + analyze_aggregate paths)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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__":