"""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}"` 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 slice — spec bodies are a LOCAL STUB pending the tool team's real # specs (KM-465 #4); the canonical NAME SET is `DATA_ACCESS_TOOLS` (tool-owned). # --------------------------------------------------------------------------- # _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])