sofhiaazzhr Claude Opus 4.8 commited on
Commit
efc0af7
Β·
1 Parent(s): db2a10f

feat(planner/help): graceful degradation for gated analytics capabilities

Browse files

When a question needs a gated/absent capability (e.g. a significance test β†’
analyze_comparison), the system used to set the user up to fail: the Help skill
would suggest the question, the planner would fall back to an unsupported
aggregation (std) and fail at runtime, and the Assembler would surrender with a
leaked "technical failure" message. Defense in depth across four layers:

- help.md v5: add a Capability boundary β€” Help only suggests analyses the live
tools can deliver (descriptive, group-by, correlation, trend); never
significance/forecasting/causal/clustering/share-of-total.
- aggregation.py: analyze_aggregate description bans std/var and adds an on-axis
graceful-degrade rule (degrade to grouped mean; never fake with std; never
jump to an unrelated tool).
- planner/validator.py: new check 8c validates aggregation FUNCTION values
against SUPPORTED_AGGS, so an unsupported func (std) is rejected at validation
with a corrective hint β€” the retry self-corrects before runtime instead of
failing silently.
- assembler.md rule #2: lead with successful results, describe limits in
business language, never leak the internal failure cause.

Verified per-component (validator rejects std/var, passes mean). Not yet tested
E2E live (needs Azure). Follow-up: make Help capability-aware from the registry
instead of the static boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

src/agents/planner/validator.py CHANGED
@@ -19,6 +19,7 @@ from ...catalog.models import Catalog
19
  from ...query.ir.models import QueryIR
20
  from ...query.ir.repair import IRRepairer
21
  from ...query.ir.validator import IRValidationError, IRValidator
 
22
  from .contracts import ToolRegistry
23
  from .errors import PlannerValidationError
24
  from .inputs import Constraints
@@ -105,6 +106,33 @@ class PlannerValidator:
105
  f"{sorted(unknown)} (allowed: {sorted(allowed)})"
106
  )
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  # Check 3 β€” concrete source_id args must exist in the catalog.
109
  src = call.args.get("source_id")
110
  if isinstance(src, str) and not _is_placeholder(src):
 
19
  from ...query.ir.models import QueryIR
20
  from ...query.ir.repair import IRRepairer
21
  from ...query.ir.validator import IRValidationError, IRValidator
22
+ from ...tools.analytics.aggregation import SUPPORTED_AGGS
23
  from .contracts import ToolRegistry
24
  from .errors import PlannerValidationError
25
  from .inputs import Constraints
 
106
  f"{sorted(unknown)} (allowed: {sorted(allowed)})"
107
  )
108
 
109
+ # Check 8c β€” analyze_aggregate: every aggregation FUNCTION must be one
110
+ # the tool supports. Check 8a only validates arg *names* (`aggregations`
111
+ # is allowed); it never looks at the function *values* inside the dict,
112
+ # so an unsupported func like `std` otherwise passes validation and only
113
+ # fails at execution β€” too late for a corrective retry, so the task
114
+ # reaches the Assembler as a silent failure. Catch it here so the planner
115
+ # is re-prompted to degrade to a supported function (e.g. `mean`).
116
+ if call.tool == "analyze_aggregate":
117
+ aggs = call.args.get("aggregations")
118
+ if isinstance(aggs, dict):
119
+ bad = sorted(
120
+ {
121
+ f
122
+ for funcs in aggs.values()
123
+ for f in ([funcs] if isinstance(funcs, str) else funcs or [])
124
+ if f not in SUPPORTED_AGGS
125
+ }
126
+ )
127
+ if bad:
128
+ raise PlannerValidationError(
129
+ f"task {task.id}: analyze_aggregate has unsupported "
130
+ f"aggregation function(s) {bad} (supported: "
131
+ f"{sorted(SUPPORTED_AGGS)}). Use a supported function "
132
+ "(e.g. mean/median); for the spread of a whole column "
133
+ "use analyze_descriptive instead."
134
+ )
135
+
136
  # Check 3 β€” concrete source_id args must exist in the catalog.
137
  src = call.args.get("source_id")
138
  if isinstance(src, str) and not _is_placeholder(src):
src/config/prompts/assembler.md CHANGED
@@ -25,9 +25,17 @@ You produce two things in one structured object:
25
  and values present in the task results. **Never invent, estimate, or extrapolate
26
  a number** that is not in the results. If the data does not answer part of the
27
  question, say so.
28
- 2. **Report what failed.** Some tasks may have `status: partial` or `failure`. Do
29
- not pretend they succeeded. Briefly state what could not be completed and how it
30
- limits the answer; put unresolved items in `open_questions`.
 
 
 
 
 
 
 
 
31
  3. **Render, don't recompute.** Build markdown tables from the structured task
32
  outputs as they are. Do not do your own arithmetic beyond trivially restating a
33
  value already computed.
 
25
  and values present in the task results. **Never invent, estimate, or extrapolate
26
  a number** that is not in the results. If the data does not answer part of the
27
  question, say so.
28
+ 2. **Report what failed β€” in plain terms, and still answer.** Some tasks may have
29
+ `status: partial` or `failure`. Do not pretend they succeeded β€” but do not lead with
30
+ the failure either. **First** give the most useful answer the SUCCESSFUL tasks
31
+ support; **then** state, in business language, what could not be determined and how
32
+ it limits the answer; put unresolved items in `open_questions`. **Never expose the
33
+ internal cause** of a failure β€” no "the tool failed", "could not compute", "technical
34
+ error", task ids, or function/tool names. Describe the limit by what it *means for the
35
+ reader*, not by what broke internally. E.g. write "a formal significance test was not
36
+ run, so this shows the difference in averages but not whether it is statistically
37
+ significant" β€” NOT "the calculation of the score distribution failed". A narrower,
38
+ honest answer beats an apology.
39
  3. **Render, don't recompute.** Build markdown tables from the structured task
40
  outputs as they are. Do not do your own arithmetic beyond trivially restating a
41
  value already computed.
src/config/prompts/help.md CHANGED
@@ -1,4 +1,9 @@
1
- <!-- help.md Β· v4 Β· Help skill prompt.
 
 
 
 
 
2
  v4 (2026-07-03): reply language relaxed from hard-"only" to DEFAULT + explicit-request
3
  exception β€” an explicit user request ("jawab dalam bahasa Inggris") now overrides the
4
  detected [Reply language]; anti-drift default (incl. synthetic-trigger protection) is
@@ -75,6 +80,35 @@ Do not over-promise the report's depth.
75
  > chat skill to fix it; gently suggest they set the objective + business questions in the New
76
  > Analysis form.
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  ## How-to phrasing (degrade gracefully)
79
 
80
  - **Via chat / skills** β€” write these **accurately and specifically**; they are stable (e.g. "type your question in the chat", "run `/report`").
@@ -102,6 +136,7 @@ spam, no overselling. A few sentences is usually enough.
102
  - Never suggest an action that the signals say isn't available or isn't ready.
103
  - One step at a time β€” give the next step, not the whole roadmap.
104
  - When you suggest questions, **dedupe against `chat_history`** β€” only propose analyses not yet run that move the goal forward; a question that already has an answer adds no fresh evidence.
 
105
  - No markdown headers or code fences in your reply; short prose (and an inline `/command` or a tiny bullet list) is fine.
106
 
107
  ## Examples
 
1
+ <!-- help.md Β· v5 Β· Help skill prompt.
2
+ v5 (2026-07-07): added the "Capability boundary" section β€” Help now only suggests
3
+ analyses the live tools can actually deliver (descriptive, group-by, correlation, trend)
4
+ and must NOT suggest significance tests, forecasting/modeling, causal claims, clustering/
5
+ segmentation, or share-of-total. Fixes Help recommending a statistical-significance
6
+ question the system has no tool for (the user copy-pasted the suggestion β†’ dead end).
7
  v4 (2026-07-03): reply language relaxed from hard-"only" to DEFAULT + explicit-request
8
  exception β€” an explicit user request ("jawab dalam bahasa Inggris") now overrides the
9
  detected [Reply language]; anti-drift default (incl. synthetic-trigger protection) is
 
80
  > chat skill to fix it; gently suggest they set the objective + business questions in the New
81
  > Analysis form.
82
 
83
+ ## Capability boundary β€” only suggest analyses the tools can actually do
84
+
85
+ Every question you propose must be answerable by the system's live analysis capabilities.
86
+ Suggesting an analysis the tools cannot perform sets the user up to fail: they copy your
87
+ suggestion, ask it, and hit a dead end. Stay strictly inside this list.
88
+
89
+ **You MAY suggest** (supported):
90
+ - Descriptive summaries of a column β€” average, median, spread, min/max, distribution.
91
+ - Group-by breakdowns β€” a total, average, or count of a metric **per category**. This is also
92
+ how to compare groups (e.g. "the average retention for online vs offline").
93
+ - Correlation / relationship between numeric columns ("which factors relate to exam score?").
94
+ - Trends over time.
95
+ - Inventory β€” what data / tables / documents exist.
96
+
97
+ **You must NOT suggest** (no tool exists β€” do not propose these even when they fit the goal):
98
+ - **Statistical significance / hypothesis tests** β€” never use "significant", "statistically
99
+ significant", "significant difference", t-test, ANOVA, or p-value. To compare groups, suggest
100
+ a group-by average that shows the gap (e.g. "compare the average retention of online vs
101
+ offline"), NOT "is the difference significant".
102
+ - Predictive modeling, forecasting, or regression models.
103
+ - Causal claims β€” avoid "cause", "impact of", "effect of" framed as causation; keep it to
104
+ relationship / correlation.
105
+ - Clustering or segmentation into discovered groups.
106
+ - Share-of-total / contribution breakdowns.
107
+
108
+ When a goal naturally invites a forbidden analysis (e.g. the user wants to know if a gap is
109
+ "real"), degrade the suggestion to the nearest supported one β€” the group-by average that shows
110
+ the gap β€” rather than promising the unsupported analysis.
111
+
112
  ## How-to phrasing (degrade gracefully)
113
 
114
  - **Via chat / skills** β€” write these **accurately and specifically**; they are stable (e.g. "type your question in the chat", "run `/report`").
 
136
  - Never suggest an action that the signals say isn't available or isn't ready.
137
  - One step at a time β€” give the next step, not the whole roadmap.
138
  - When you suggest questions, **dedupe against `chat_history`** β€” only propose analyses not yet run that move the goal forward; a question that already has an answer adds no fresh evidence.
139
+ - **Stay inside the Capability boundary above** β€” never propose significance tests, forecasting, modeling, causal claims, clustering/segmentation, or share-of-total; there is no tool for them, so suggesting them sends the user into a dead end.
140
  - No markdown headers or code fences in your reply; short prose (and an inline `/command` or a tiny bullet list) is fine.
141
 
142
  ## Examples
src/tools/analytics/aggregation.py CHANGED
@@ -44,8 +44,12 @@ def _clean(value: object) -> object:
44
  # Final destination is ToolSpec.description once the wrapper layer is built.
45
  DESCRIPTION = """\
46
  Summary: Group-by aggregation. Splits rows by one or more key columns and \
47
- computes aggregates per group (sum, mean, count, min, max, median, nunique). \
48
- Returns one row per group.
 
 
 
 
49
 
50
  USE WHEN the question groups a metric by a category β€” the tell-tale sign is \
51
  "per"/"each"/"by" a dimension. Trigger words: "per/each" (per/tiap), "by" \
@@ -57,6 +61,15 @@ DON'T USE WHEN:
57
  - it splits a single total into shares -> analyze_contribution
58
  - the grouping is over time periods -> analyze_trend
59
 
 
 
 
 
 
 
 
 
 
60
  Example questions:
61
  - "total revenue per region"
62
  - "average order value by customer segment"
 
44
  # Final destination is ToolSpec.description once the wrapper layer is built.
45
  DESCRIPTION = """\
46
  Summary: Group-by aggregation. Splits rows by one or more key columns and \
47
+ computes aggregates per group. Returns one row per group.
48
+
49
+ SUPPORTED FUNCTIONS β€” use ONLY these: sum, mean, count, min, max, median, \
50
+ nunique. Standard deviation / variance are NOT available here: for the spread of \
51
+ a whole column use analyze_descriptive (whole-column only β€” spread PER GROUP is \
52
+ not available in v1). NEVER pass std / var / stdev to this tool; it will fail.
53
 
54
  USE WHEN the question groups a metric by a category β€” the tell-tale sign is \
55
  "per"/"each"/"by" a dimension. Trigger words: "per/each" (per/tiap), "by" \
 
61
  - it splits a single total into shares -> analyze_contribution
62
  - the grouping is over time periods -> analyze_trend
63
 
64
+ GRACEFUL DEGRADE β€” if a tool named above (e.g. analyze_comparison) is NOT present \
65
+ in the "Available tools" list, do NOT fake it with an unsupported aggregation \
66
+ (like std) and do NOT abandon the task. Degrade to an answer that stays on the \
67
+ SAME question: group the measure by the group column with `mean` (optionally \
68
+ min/max/median) so each group's level and their difference are still shown. Do \
69
+ NOT switch to an unrelated tool just to produce output. The Assembler will note \
70
+ that a significance test / per-group spread was not computed β€” a narrower, true \
71
+ answer beats a failed one.
72
+
73
  Example questions:
74
  - "total revenue per region"
75
  - "average order value by customer segment"