Rifqi Hafizuddin
[KM-715] charts: render_chart tool + message_charts store + GET /api/v1/charts + planner viz slice
87cfcf8 | """Analytics tool registry | |
| The real registry of the `analyze_*` family, built on the canonical `ToolSpec` | |
| (src/tools/contracts.py) and the prompt-style `DESCRIPTION` constants the Planner | |
| reads to choose a tool (KM-625). This replaces the agent team's local stub in | |
| `src/agents/planner/registry.py` for the analytics slice. | |
| Conventions (decided with the agent team, KM-465): | |
| - **Pattern A** — `analyze_*` tools do NOT self-fetch by `source_id`. Each takes a | |
| `data` argument that is a `"${t<id>}"` placeholder pointing at an upstream | |
| `retrieve_data` table output, resolved to a DataFrame at execution time. | |
| Column arguments reference the aliases that upstream query produced. | |
| - `input_schema` is the lightweight JSON-schema-ish dict the planner validator | |
| consumes: `required` (arg names with no default) + `properties` (allowed args). | |
| `required` mirrors each compute function's no-default parameters; value-typing of | |
| placeholder args is deferred to execution time. | |
| - `output_kind` is the `ToolOutput.kind` each tool returns: stats (labelled-metric | |
| dict) | table (rows×cols) | series (ordered periods). | |
| The four data-access tools (retrieve_data / retrieve_knowledge / check_data / | |
| check_knowledge) are registered separately once their wrappers land (KM-465 #4); | |
| `default_registry()` composes both slices. | |
| """ | |
| from __future__ import annotations | |
| from src.tools.analytics import ( | |
| aggregation, | |
| comparison, | |
| decomposition, | |
| descriptive, | |
| merge, | |
| quality, | |
| relationship, | |
| segmentation, | |
| temporal, | |
| visualization, | |
| ) | |
| from src.tools.contracts import ToolRegistry, ToolSpec | |
| # Active this round — the analytics (+ render_chart, SPINE_V2_PLAN §4.1) tools | |
| # the Planner may select. | |
| ACTIVE_ANALYTICS_TOOLS: list[ToolSpec] = [ | |
| ToolSpec( | |
| name="analyze_descriptive", | |
| category="analytics.descriptive", | |
| input_schema={ | |
| "required": ["data", "column_ids"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "column_ids": {"type": "array"}, | |
| "metrics": {"type": "array"}, | |
| }, | |
| }, | |
| output_kind="stats", | |
| description=descriptive.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_aggregate", | |
| category="analytics.aggregation", | |
| input_schema={ | |
| "required": ["data", "aggregations"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "aggregations": {"type": "object"}, | |
| "group_by": {"type": "array"}, | |
| }, | |
| }, | |
| output_kind="table", | |
| description=aggregation.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_correlation", | |
| category="analytics.relationship", | |
| input_schema={ | |
| "required": ["data"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "column_ids": {"type": "array"}, | |
| "method": {"type": "string"}, | |
| }, | |
| }, | |
| output_kind="stats", | |
| description=relationship.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_trend", | |
| category="analytics.timeseries", | |
| input_schema={ | |
| "required": ["data", "date_column", "value_column"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "date_column": {"type": "string"}, | |
| "value_column": {"type": "string"}, | |
| "freq": {"type": "string"}, | |
| "agg": {"type": "string"}, | |
| }, | |
| }, | |
| output_kind="series", | |
| description=temporal.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_merge", | |
| category="analytics.combine", | |
| input_schema={ | |
| "required": ["data", "data_right", "on"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "data_right": {"type": "string"}, | |
| "on": {"type": "array"}, | |
| "how": {"type": "string"}, | |
| "suffixes": {"type": "array"}, | |
| }, | |
| }, | |
| output_kind="table", | |
| description=merge.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="render_chart", | |
| category="analytics.visualization", | |
| input_schema={ | |
| "required": ["data", "chart_type", "x", "y"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "chart_type": {"type": "string"}, | |
| "x": {"type": "string"}, | |
| "y": {"type": "string"}, | |
| "series": {"type": "string"}, | |
| "title": {"type": "string"}, | |
| }, | |
| }, | |
| output_kind="chart", | |
| description=visualization.DESCRIPTION, | |
| ), | |
| ] | |
| # Deferred this round — specs kept intact for easy re-activation, NOT exposed to | |
| # the Planner. The compute fns still exist (src/tools/analytics/*) and the invoker | |
| # still maps them (src/tools/invoker.py); only registry exposure is withheld. | |
| # To re-activate, move a spec back into ACTIVE_ANALYTICS_TOOLS. NOTE: a deferred | |
| # tool re-activated here must also be re-added to the Planner few-shots | |
| # (src/agents/planner/examples.py) — keep the two in sync. | |
| DEFERRED_ANALYTICS_TOOLS: list[ToolSpec] = [ | |
| ToolSpec( | |
| name="analyze_comparison", | |
| category="analytics.comparison", | |
| input_schema={ | |
| "required": ["data", "dimension", "value_column", "group_a", "group_b"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "dimension": {"type": "string"}, | |
| "value_column": {"type": "string"}, | |
| "group_a": {}, | |
| "group_b": {}, | |
| "agg": {"type": "string"}, | |
| }, | |
| }, | |
| output_kind="stats", | |
| description=comparison.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_contribution", | |
| category="analytics.decomposition", | |
| input_schema={ | |
| "required": ["data", "dimension", "value_column"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "dimension": {"type": "string"}, | |
| "value_column": {"type": "string"}, | |
| "agg": {"type": "string"}, | |
| "top_n": {"type": "integer"}, | |
| }, | |
| }, | |
| output_kind="table", | |
| description=decomposition.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_profile", | |
| category="analytics.quality", | |
| input_schema={ | |
| "required": ["data"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "column_ids": {"type": "array"}, | |
| }, | |
| }, | |
| output_kind="stats", | |
| description=quality.DESCRIPTION, | |
| ), | |
| ToolSpec( | |
| name="analyze_segment", | |
| category="analytics.segmentation", | |
| input_schema={ | |
| "required": ["data", "column", "bins"], | |
| "properties": { | |
| "data": {"type": "string"}, | |
| "column": {"type": "string"}, | |
| "bins": {}, | |
| "method": {"type": "string"}, | |
| "labels": {"type": "array"}, | |
| "value_column": {"type": "string"}, | |
| "agg": {"type": "string"}, | |
| }, | |
| }, | |
| output_kind="table", | |
| description=segmentation.DESCRIPTION, | |
| ), | |
| ] | |
| # Full set (active + deferred) — kept for callers that need every spec, e.g. tests | |
| # or the invoker's name checks. The Planner-visible registry uses ACTIVE only. | |
| ANALYTICS_TOOLS: list[ToolSpec] = [*ACTIVE_ANALYTICS_TOOLS, *DEFERRED_ANALYTICS_TOOLS] | |
| def analytics_registry() -> ToolRegistry: | |
| """The analytics (`analyze_*`) slice of the tool registry (fresh instance). | |
| Exposes only `ACTIVE_ANALYTICS_TOOLS`; deferred specs are withheld from the | |
| Planner (see `DEFERRED_ANALYTICS_TOOLS`). | |
| """ | |
| return ToolRegistry(tools=list(ACTIVE_ANALYTICS_TOOLS)) | |