Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit
c08395e
·
1 Parent(s): 421cf11

[KM-626][AI] Planner: consume teammate's real analytics registry

Browse files

After the tool team landed the canonical contracts + real analytics registry
(KM-627/628), reconcile our planner-facing registry to the real thing instead of
the stub:

- default_registry() now composes the REAL analytics slice (src/tools/registry.py
::analytics_registry()) + a local stub for only the 4 data-access tools (still
pending KM-465 #4).
- Drops our 8 analyze_* stub entries; removes the one drift (their
analyze_descriptive requires only ["data"], column_ids optional) and adopts the
real prompt-style tool descriptions.
- Pattern A is now confirmed (KM-465); no agent-code change needed (INV-7).

Verified: 44 planner+slow_path tests green, ruff clean. No changes to src/tools
(tool team owned).

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

Files changed (1) hide show
  1. src/agents/planner/registry.py +31 -228
src/agents/planner/registry.py CHANGED
@@ -1,55 +1,38 @@
1
- """STUB v1 tool registry composite ("family") tools.
 
2
 
3
- This is the agent team's local stand-in for the tool team's inventory (KM-608)
4
- so the planner is buildable and testable before the real wrapper layer lands.
5
- The tools here are *contracts only* — the compute logic for the `analyze_*`
6
- family already exists in `src/tools/analytics/` (KM-624), but the wrapper layer
7
- (source/placeholder -> DataFrame fetch, the `ToolOutput` envelope, never-throw
8
- error handling, ToolSpec registration) is still pending the Planner seam
9
- (KM-418 / AGENT_ARCHITECTURE_CONTEXT_new.md §8.4). The planner plans against the
10
- registry and never names a tool outside it (INV-7).
11
 
12
- **Taxonomy decision (2026-06-08):** v1 uses **composite/family** tools, not the
13
- atomic `compute_*` set the earlier draft assumed. One `analyze_*` call does a
14
- whole analytical job (e.g. `analyze_descriptive` returns mean/median/mode/std/
15
- quartiles/skew/null_rate at once, replacing four atomic `compute_*` tools). See
16
- §9.3 / the decisions table in the architecture doc.
 
 
 
 
17
 
18
- **Ownership (revised 2026-06-08): the tool team owns ALL tools** compute,
19
- data-access (`query_structured`/`retrieve_documents`/`list_sources`/
20
- `describe_source`), the wrapper/invoker, and tests. This file is purely the agent
21
- team's local scaffold for building/testing the Planner (and later the TaskRunner/
22
- Assembler against mocks) until the real registry lands; replace it then.
23
-
24
- **Data-flow convention (Pattern A — assumed, but the tool team's call, still open):**
25
- this stub assumes the `analyze_*` tools do NOT self-fetch by `source_id`; each
26
- takes a `data` argument that is a `"${t<id>}"` placeholder pointing at an upstream
27
- `query_structured` table output, resolved to a DataFrame at execution time. Column
28
- arguments (`column_ids`, `dimension`, `value_column`, `date_column`, …) reference
29
- the *aliases* the upstream query produced. If the tool team instead picks Pattern B
30
- (self-fetch by `source_id`), reshape this stub + the few-shot examples to match —
31
- the agent code does not change either way (INV-7).
32
-
33
- `input_schema` is the lightweight JSON-schema-ish dict the planner validator
34
- (validator.py check #8) consumes: `required` (list of arg names) + `properties`
35
- (allowed arg names). Arg *values* may be `"${t<id>}"` placeholders resolved at
36
- execution time, so the validator checks arg *keys*, not value types — except
37
- `query_structured.args["ir"]`, whose inline QueryIR is validated against the
38
  catalog by the existing IRValidator.
39
 
40
- When KM-608/KM-418 ship, replace `default_registry()` with the real registry
41
- import. See AGENT_ARCHITECTURE_CONTEXT_new.md §9.2 / §9.3.
42
  """
43
 
44
  from __future__ import annotations
45
 
 
 
46
  from .contracts import ToolRegistry, ToolSpec
47
 
48
- _P0_TOOLS: list[ToolSpec] = [
49
- # ----------------------------------------------------------------------- #
50
- # Data access + catalog introspection (agent-team owned; wrap existing
51
- # Phase 2 infra — QueryService / RetrievalRouter / CatalogReader).
52
- # ----------------------------------------------------------------------- #
53
  ToolSpec(
54
  name="query_structured",
55
  category="analytics.query",
@@ -112,194 +95,14 @@ _P0_TOOLS: list[ToolSpec] = [
112
  "before querying it. Do NOT use it to fetch data rows (use query_structured)."
113
  ),
114
  ),
115
- # ----------------------------------------------------------------------- #
116
- # Analytics family (KM-624 compute; wrapper pending). Each takes `data` =
117
- # a "${t<id>}" placeholder for an upstream query_structured table output.
118
- # ----------------------------------------------------------------------- #
119
- ToolSpec(
120
- name="analyze_descriptive",
121
- category="analytics.descriptive",
122
- input_schema={
123
- "required": ["data", "column_ids"],
124
- "properties": {
125
- "data": {"type": "string"},
126
- "column_ids": {"type": "array"},
127
- "metrics": {"type": "array"},
128
- },
129
- },
130
- output_kind="stats",
131
- description=(
132
- "Single/multi-column EDA in one call: count, mean, median, mode, std, "
133
- "variance, quartiles (q1/q3), min, max, skew, null_count, null_rate for each "
134
- "of `column_ids`. `data` is a '${t<id>}' placeholder for an upstream "
135
- "query_structured result; `column_ids` are that result's column aliases. "
136
- "This replaces the atomic compute_median/mode/stddev/percentile tools — ask "
137
- "for the whole profile, not one statistic at a time. Do NOT use it for "
138
- "group-by aggregates (analyze_aggregate) or time trends (analyze_trend)."
139
- ),
140
- ),
141
- ToolSpec(
142
- name="analyze_aggregate",
143
- category="analytics.aggregation",
144
- input_schema={
145
- "required": ["data", "aggregations"],
146
- "properties": {
147
- "data": {"type": "string"},
148
- "aggregations": {"type": "object"},
149
- "group_by": {"type": "array"},
150
- },
151
- },
152
- output_kind="table",
153
- description=(
154
- "Group-by aggregation over an already-materialized result: per group, "
155
- "compute `aggregations` like {\"revenue\": [\"sum\", \"mean\"], "
156
- "\"order_id\": [\"count\"]} (sum/mean/count/min/max/median/nunique). `data` "
157
- "is a '${t<id>}' placeholder; `group_by` columns and aggregated columns are "
158
- "that result's aliases. Prefer query_structured for simple group-by the IR "
159
- "can already express; use this to aggregate a derived/joined/intermediate "
160
- "result, or for median per group (the IR cannot)."
161
- ),
162
- ),
163
- ToolSpec(
164
- name="analyze_comparison",
165
- category="analytics.comparison",
166
- input_schema={
167
- "required": ["data", "dimension", "value_column", "group_a", "group_b"],
168
- "properties": {
169
- "data": {"type": "string"},
170
- "dimension": {"type": "string"},
171
- "value_column": {"type": "string"},
172
- "group_a": {},
173
- "group_b": {},
174
- "agg": {"type": "string"},
175
- },
176
- },
177
- output_kind="stats",
178
- description=(
179
- "Compare one aggregated metric between two groups of a dimension (e.g. "
180
- "region 'A' vs 'B'): returns each group's value, absolute and percent "
181
- "difference, and direction (higher/lower/equal); group_a is the baseline. "
182
- "`data` is a '${t<id>}' placeholder; `dimension`/`value_column` are aliases; "
183
- "`agg` defaults to sum. Use for exactly TWO groups. For many categories' "
184
- "share of a total use analyze_contribution; for movement over time use "
185
- "analyze_trend."
186
- ),
187
- ),
188
- ToolSpec(
189
- name="analyze_contribution",
190
- category="analytics.decomposition",
191
- input_schema={
192
- "required": ["data", "dimension", "value_column"],
193
- "properties": {
194
- "data": {"type": "string"},
195
- "dimension": {"type": "string"},
196
- "value_column": {"type": "string"},
197
- "agg": {"type": "string"},
198
- "top_n": {"type": "integer"},
199
- },
200
- },
201
- output_kind="table",
202
- description=(
203
- "Share-of-total breakdown: each category's value, share, and running "
204
- "cumulative share, largest first — the tool for 'which categories drive "
205
- "most of X?' and Pareto (80/20) reasoning. `data` is a '${t<id>}' "
206
- "placeholder; `dimension`/`value_column` are aliases; `agg` defaults to sum; "
207
- "`top_n` lumps the tail into an 'Others' row. Use for a single snapshot of "
208
- "many categories. Do NOT use it to compare exactly two groups "
209
- "(analyze_comparison) or to trend over time (analyze_trend)."
210
- ),
211
- ),
212
- ToolSpec(
213
- name="analyze_profile",
214
- category="analytics.quality",
215
- input_schema={
216
- "required": ["data"],
217
- "properties": {"data": {"type": "string"}, "column_ids": {"type": "array"}},
218
- },
219
- output_kind="stats",
220
- description=(
221
- "Per-column data-quality profile: dtype, inferred type, completeness "
222
- "(null_count/null_rate), cardinality (distinct_count/rate, is_constant), and "
223
- "for numeric columns min/max/mean plus an IQR-based outlier_count (top value "
224
- "for non-numeric). `data` is a '${t<id>}' placeholder; `column_ids` defaults "
225
- "to all columns. Use in data_understanding to judge whether data is clean "
226
- "enough before deeper analysis. Do NOT use it for the analytical answer "
227
- "itself — it describes data health, not the business metric."
228
- ),
229
- ),
230
- ToolSpec(
231
- name="analyze_correlation",
232
- category="analytics.relationship",
233
- input_schema={
234
- "required": ["data"],
235
- "properties": {
236
- "data": {"type": "string"},
237
- "column_ids": {"type": "array"},
238
- "method": {"type": "string"},
239
- },
240
- },
241
- output_kind="stats",
242
- description=(
243
- "Pairwise correlation across numeric columns: returns the full matrix plus "
244
- "column pairs ranked by strength. `data` is a '${t<id>}' placeholder; "
245
- "`column_ids` defaults to all numeric columns; `method` is pearson "
246
- "(default), spearman, or kendall. Use for 'does X relate to Y?'. Needs at "
247
- "least two numeric columns. Correlation is not causation — it does not "
248
- "explain why, and is not a model."
249
- ),
250
- ),
251
- ToolSpec(
252
- name="analyze_segment",
253
- category="analytics.segmentation",
254
- input_schema={
255
- "required": ["data", "column", "bins"],
256
- "properties": {
257
- "data": {"type": "string"},
258
- "column": {"type": "string"},
259
- "bins": {},
260
- "method": {"type": "string"},
261
- "labels": {"type": "array"},
262
- "value_column": {"type": "string"},
263
- "agg": {"type": "string"},
264
- },
265
- },
266
- output_kind="table",
267
- description=(
268
- "Bucket rows by binning a numeric `column` and report how rows distribute "
269
- "across segments (count, and optionally an aggregate of `value_column` per "
270
- "segment). `method` 'edges' takes explicit boundaries in `bins` (e.g. "
271
- "[0,18,35,60]); 'quantile' takes an integer bucket count (e.g. 4 for "
272
- "quartiles). `data` is a '${t<id>}' placeholder; columns are aliases. Use "
273
- "for age brackets, value tiers, etc. The binned column must be numeric."
274
- ),
275
- ),
276
- ToolSpec(
277
- name="analyze_trend",
278
- category="analytics.timeseries",
279
- input_schema={
280
- "required": ["data", "date_column", "value_column"],
281
- "properties": {
282
- "data": {"type": "string"},
283
- "date_column": {"type": "string"},
284
- "value_column": {"type": "string"},
285
- "freq": {"type": "string"},
286
- "agg": {"type": "string"},
287
- },
288
- },
289
- output_kind="series",
290
- description=(
291
- "Time-series trend in one call: bucket rows into periods (`freq` = "
292
- "day/week/month/quarter/year), aggregate `value_column` per period (`agg` "
293
- "defaults to sum), and summarize movement (per-period points, first vs last, "
294
- "absolute/percent change, direction, linear slope). `data` is a '${t<id>}' "
295
- "placeholder; `date_column`/`value_column` are aliases from the upstream "
296
- "query. This replaces the atomic date_trunc tool. Do NOT use it to filter by "
297
- "date — put the date filter in the query_structured IR instead."
298
- ),
299
- ),
300
  ]
301
 
302
 
303
  def default_registry() -> ToolRegistry:
304
- """The v1 stub registry (a fresh instance per call)."""
305
- return ToolRegistry(tools=list(_P0_TOOLS))
 
 
 
 
 
 
1
+ """v1 tool registry the Planner plans against (INV-7: agent never names a tool
2
+ outside it).
3
 
4
+ **Composed from two slices (2026-06-08):**
 
 
 
 
 
 
 
5
 
6
+ - **Analytics (`analyze_*`) REAL, tool-team-owned.** Sourced live from
7
+ `src/tools/registry.py::analytics_registry()` (KM-628), built on the canonical
8
+ `ToolSpec` (`src/tools/contracts.py`, KM-465/KM-627) and the prompt-style tool
9
+ descriptions (KM-625). No longer a stub on our side — it tracks the real registry.
10
+ - **Data access (`query_structured` / `retrieve_documents` / `list_sources` /
11
+ `describe_source`) — STILL A LOCAL STUB.** The tool team owns these too, but their
12
+ wrappers + `ToolSpec`s haven't landed yet (KM-465 #4). We keep best-guess specs
13
+ here so the Planner can plan end-to-end; when the real ones ship, delete this slice
14
+ and swap `default_registry()` for the tool team's full composition.
15
 
16
+ **Confirmed conventions (KM-465):** Pattern A `analyze_*` tools take a `data`
17
+ `"${t<id>}"` placeholder pointing at an upstream `query_structured` output (no
18
+ self-fetch); resolved to a DataFrame at execution time. `input_schema` is the
19
+ lightweight `{required, properties}` dict the planner validator (check #8) reads;
20
+ `query_structured.args["ir"]` carries an inline QueryIR validated against the
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  catalog by the existing IRValidator.
22
 
23
+ See AGENT_ARCHITECTURE_CONTEXT_new.md §9.2 / §9.3.
 
24
  """
25
 
26
  from __future__ import annotations
27
 
28
+ from src.tools.registry import analytics_registry
29
+
30
  from .contracts import ToolRegistry, ToolSpec
31
 
32
+ # --------------------------------------------------------------------------- #
33
+ # Data-access slice — LOCAL STUB pending the tool team's real specs (KM-465 #4).
34
+ # --------------------------------------------------------------------------- #
35
+ _DATA_ACCESS_TOOLS: list[ToolSpec] = [
 
36
  ToolSpec(
37
  name="query_structured",
38
  category="analytics.query",
 
95
  "before querying it. Do NOT use it to fetch data rows (use query_structured)."
96
  ),
97
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  ]
99
 
100
 
101
  def default_registry() -> ToolRegistry:
102
+ """The v1 registry: stub data-access slice + the real analytics slice.
103
+
104
+ The analytics tools come live from `src.tools.registry` (the tool team's real
105
+ registry); only the data-access slice is still a local stub. A fresh instance
106
+ per call.
107
+ """
108
+ return ToolRegistry(tools=[*_DATA_ACCESS_TOOLS, *analytics_registry().tools])