feat(planner/help): graceful degradation for gated analytics capabilities
Browse filesWhen 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 +28 -0
- src/config/prompts/assembler.md +11 -3
- src/config/prompts/help.md +36 -1
- src/tools/analytics/aggregation.py +15 -2
|
@@ -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):
|
|
@@ -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
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
@@ -1,4 +1,9 @@
|
|
| 1 |
-
<!-- help.md Β·
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -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
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|