| """v1 tool registry the Planner plans against (INV-7: agent never names a tool |
| outside it). |
| |
| **Composed from two slices (2026-06-08):** |
| |
| - **Analytics (`analyze_*`) β REAL, tool-team-owned.** Sourced live from |
| `src/tools/registry.py::analytics_registry()` (KM-628), built on the canonical |
| `ToolSpec` (`src/tools/contracts.py`, KM-465/KM-627) and the prompt-style tool |
| descriptions (KM-625). No longer a stub on our side β it tracks the real registry. |
| - **Data access (`retrieve_data` / `retrieve_knowledge` / `check_data` / |
| `check_knowledge`) β spec BODIES still a local stub.** The tool team owns these too, |
| but their wrappers + `ToolSpec`s haven't landed yet (KM-465 #4). We keep best-guess |
| spec bodies here so the Planner can plan end-to-end β but the NAMES derive from |
| `src.tools.data_access.DATA_ACCESS_TOOLS` (R11), so a tool rename/addition upstream |
| fails loudly here instead of drifting silently. When the real specs ship, delete |
| this slice and swap `default_registry()` for the tool team's full composition. |
| |
| **Confirmed conventions (KM-465):** Pattern A β `analyze_*` tools take a `data` |
| `"${t<id>}"` placeholder pointing at an upstream `retrieve_data` output (no |
| self-fetch); resolved to a DataFrame at execution time. `input_schema` is the |
| lightweight `{required, properties}` dict the planner validator (check #8) reads; |
| `retrieve_data.args["ir"]` carries an inline QueryIR validated against the |
| catalog by the existing IRValidator. |
| |
| See AGENT_ARCHITECTURE_CONTEXT_new.md Β§9.2 / Β§9.3. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from src.tools.data_access import DATA_ACCESS_TOOLS |
| from src.tools.registry import analytics_registry |
|
|
| from .contracts import ToolRegistry, ToolSpec |
|
|
| |
| |
| |
| |
| _DATA_ACCESS_SPEC_BODIES: tuple[ToolSpec, ...] = ( |
| ToolSpec( |
| name="retrieve_data", |
| category="analytics.query", |
| input_schema={"required": ["ir"], "properties": {"ir": {"type": "object"}}}, |
| output_kind="table", |
| description=( |
| "Run one validated query against a structured source and return rows. The " |
| "`ir` argument is an inline QueryIR (the JSON intent: source_id, table_id, " |
| "joins, select, filters, group_by, order_by, limit) β never SQL. This is the " |
| "data-access entry point: use it to select, filter, and pull the rows the " |
| "analytics (`analyze_*`) tools then consume. It also does simple built-in " |
| "aggregation the IR can express (count/sum/avg/min/max/count_distinct). " |
| "JOINS (database sources only): to group a measure in one table by a " |
| "dimension in a RELATED table, add a `joins` entry " |
| "({target_table_id, left_column_id, right_column_id}) along a declared " |
| "foreign key β e.g. sum order_items.line_total grouped by products.category " |
| "via order_items.product_id = products.id. Prefer an existing measure column " |
| "(e.g. line_total) over recomputing, and a single table when the measure and " |
| "dimension already live together. Joins are NOT supported on tabular/file " |
| "sources yet. Do NOT use this for richer statistics " |
| "(median/percentile/mode/stddev/skew β analyze_descriptive), trends " |
| "(analyze_trend), correlation, segmentation, or share-of-total; and do NOT " |
| "use it to read documents (use retrieve_knowledge)." |
| ), |
| ), |
| ToolSpec( |
| name="retrieve_knowledge", |
| category="retrieval.documents", |
| input_schema={ |
| "required": ["query"], |
| "properties": { |
| "query": {"type": "string"}, |
| "source_id": {"type": "string"}, |
| "top_k": {"type": "integer"}, |
| }, |
| }, |
| output_kind="documents", |
| description=( |
| "Dense-retrieve the most relevant chunks from the user's unstructured " |
| "sources (PDF/DOCX/TXT) for a natural-language `query`. Use this to pull " |
| "qualitative context into an analysis. Optionally scope to one `source_id`. " |
| "Do NOT use it for numbers in tables β that is retrieve_data's job." |
| ), |
| ), |
| ToolSpec( |
| name="check_data", |
| category="catalog.introspection", |
| input_schema={ |
| "required": [], |
| "properties": {"source_id": {"type": "string"}}, |
| }, |
| output_kind="table", |
| description=( |
| "Inspect the user's structured data sources (DB + tabular). With no " |
| "arguments, lists the sources (id, name, type, table count) β use early in " |
| "data_understanding to discover what exists. With a `source_id`, returns that " |
| "source's tables and columns (names, types, row counts) β use to confirm a " |
| "source's shape before querying it. Cheap. Do NOT use it to fetch data rows " |
| "(use retrieve_data) or to inspect documents (use check_knowledge)." |
| ), |
| ), |
| ToolSpec( |
| name="check_knowledge", |
| category="catalog.introspection", |
| input_schema={"required": [], "properties": {}}, |
| output_kind="table", |
| description=( |
| "List the user's unstructured sources / documents (id, name, type). Use in " |
| "data_understanding to discover what qualitative material exists before " |
| "retrieving from it. Do NOT use it to read document content (use " |
| "retrieve_knowledge) or to inspect structured data (use check_data)." |
| ), |
| ), |
| ) |
|
|
| _DATA_ACCESS_SPECS: dict[str, ToolSpec] = {s.name: s for s in _DATA_ACCESS_SPEC_BODIES} |
|
|
|
|
| def _data_access_slice() -> list[ToolSpec]: |
| """Data-access specs in body order, with names checked against the tool layer. |
| |
| `DATA_ACCESS_TOOLS` (src.tools.data_access) is the canonical name set; the |
| spec bodies above are still our local stub. Any mismatch (a tool added, |
| renamed, or removed upstream) raises here instead of drifting silently. |
| """ |
| if set(_DATA_ACCESS_SPECS) != DATA_ACCESS_TOOLS: |
| missing = sorted(DATA_ACCESS_TOOLS - _DATA_ACCESS_SPECS.keys()) |
| stale = sorted(_DATA_ACCESS_SPECS.keys() - DATA_ACCESS_TOOLS) |
| raise RuntimeError( |
| "planner data-access specs out of sync with " |
| f"src.tools.data_access.DATA_ACCESS_TOOLS: missing spec for {missing}, " |
| f"stale spec for {stale}" |
| ) |
| return list(_DATA_ACCESS_SPECS.values()) |
|
|
|
|
| def default_registry() -> ToolRegistry: |
| """The v1 registry: stub data-access slice + the real analytics slice. |
| |
| The analytics tools come live from `src.tools.registry` (the tool team's real |
| registry); the data-access spec bodies are still a local stub, name-checked |
| against `DATA_ACCESS_TOOLS`. A fresh instance per call. |
| """ |
| return ToolRegistry(tools=[*_data_access_slice(), *analytics_registry().tools]) |
|
|