ishaq101's picture
feat/Knowledge & Data Tools (#3)
0721bb4
Raw
History Blame Contribute Delete
7.22 kB
"""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 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])