"""Tool-catalog loading for the embedding tool selector. A *pack* is JSON in the standard function-calling shape shared by OpenAI function calling, plain JSON-Schema tool definitions, and MCP ``tools/list``:: {"name": "ecommerce", "tools": [ {"name": "search_products", "description": "Search the catalog by keyword and filters.", "parameters": {"type": "object", "properties": { "query": {"type": "string"}, "category": {"type": "string", "enum": ["lighting", "rugs"]}}, "required": ["query"]}}]} Seven curated packs ship in ``packs/`` (ecommerce, devops, travel, support, finance, healthcare, workplace — 151 tools total). They are merged into one catalog and embedded once at startup; typing a request retrieves the few tools that matter. The loader is liberal in what it accepts (a ``{"tools": [...]}`` wrapper, an MCP result with params under ``inputSchema``, a bare OpenAI tools array, or a single tool), so any catalog that speaks the schema works unchanged. Enum-valued parameters become part of a tool's indexed text: the discriminating word in a request is often a *value* ("aws", "business class", "urgent"), so surfacing the allowed vocabulary lets the retriever match it. """ from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path from typing import Any PACKS_DIR = Path(__file__).resolve().parent / "packs" @dataclass(frozen=True) class Parameter: """One tool parameter parsed from a JSON-Schema property.""" name: str type: str description: str enum: tuple[str, ...] | None required: bool @property def is_enum(self) -> bool: return self.enum is not None and len(self.enum) > 0 @dataclass(frozen=True) class Tool: """A single callable tool, its parameters, and the pack it came from.""" name: str description: str domain: str = "" parameters: tuple[Parameter, ...] = () keywords: tuple[str, ...] = () @property def routing_text(self) -> str: """Text indexed for retrieval: name + description + parameter names + enum values + keywords. Including the allowed enum values markedly improves routing on mixed catalogs (combined-catalog R@1 0.82 -> 0.95 on the original eval): the discriminating word is often a value ("aws", "business class", "urgent"), so surfacing the vocabulary lets the retriever match it. ``keywords`` is *document expansion* for implicit queries: a 350M retriever does not reliably infer "table" -> "furniture" -> product catalog, so we add the concrete words users say ("table", "lamp", "desk") to the indexed text. The words are index-only — they never appear in the card or the JSON tool definition. """ desc = self.description.strip() base = f"{self.name}: {desc}" if desc else self.name extras: list[str] = [] if self.parameters: extras.append("parameters: " + ", ".join(p.name.replace("_", " ") for p in self.parameters)) values = [v.replace("_", " ") for p in self.parameters if p.is_enum for v in (p.enum or ())] if values: extras.append("options: " + ", ".join(values)) if self.keywords: extras.append("examples: " + ", ".join(self.keywords)) return f"{base} ({'; '.join(extras)})" if extras else base def to_schema(self) -> dict[str, Any]: """The standard function-calling schema — exactly what a downstream LLM receives.""" properties: dict[str, Any] = {} required: list[str] = [] for p in self.parameters: spec: dict[str, Any] = {"type": p.type} if p.description: spec["description"] = p.description if p.enum: spec["enum"] = list(p.enum) properties[p.name] = spec if p.required: required.append(p.name) params: dict[str, Any] = {"type": "object", "properties": properties} if required: params["required"] = required return {"name": self.name, "description": self.description, "parameters": params} def to_public(self) -> dict[str, Any]: """JSON-able view sent to the frontend: card data + the full tool definition.""" return { "name": self.name, "description": self.description, "domain": self.domain, "params": [ {"name": p.name, "type": p.type, "description": p.description, "enum": list(p.enum) if p.enum else None, "required": p.required} for p in self.parameters ], "schema": self.to_schema(), } class ToolSetError(ValueError): """Raised when a payload cannot be parsed as a pack.""" def _parse_parameters(schema: dict[str, Any]) -> tuple[Parameter, ...]: properties = schema.get("properties") if not isinstance(properties, dict): return () required_raw = schema.get("required", []) required = {str(r) for r in required_raw} if isinstance(required_raw, list) else set() params: list[Parameter] = [] for raw_name, raw_spec in properties.items(): spec = raw_spec if isinstance(raw_spec, dict) else {} enum_raw = spec.get("enum") enum = tuple(str(v) for v in enum_raw) if isinstance(enum_raw, list) and enum_raw else None params.append( Parameter( name=str(raw_name), type=str(spec.get("type", "string")), description=str(spec.get("description", "")), enum=enum, required=str(raw_name) in required, ) ) return tuple(params) def tool_from_dict(raw: dict[str, Any], domain: str = "") -> Tool: """Parse one tool dict (OpenAI/JSON-Schema/MCP), tolerant of the common shapes.""" if raw.get("type") == "function" and isinstance(raw.get("function"), dict): raw = raw["function"] # OpenAI wraps each tool as {"type": "function", "function": {...}} name = raw.get("name") if not isinstance(name, str) or not name: raise ToolSetError("each tool needs a non-empty 'name'") schema = raw.get("parameters") # JSON-Schema/OpenAI use 'parameters'; MCP uses 'inputSchema' if not isinstance(schema, dict): schema = raw.get("inputSchema") parameters = _parse_parameters(schema) if isinstance(schema, dict) else () kw_raw = raw.get("keywords") # optional index-only document expansion for implicit queries keywords = tuple(str(k) for k in kw_raw) if isinstance(kw_raw, list) else () return Tool(name=name, description=str(raw.get("description", "")), domain=domain, parameters=parameters, keywords=keywords) def _extract(data: Any) -> tuple[str, list[dict[str, Any]]]: """Pull (pack_name, [tool_dict, ...]) out of any accepted top-level shape.""" if isinstance(data, list): return "tools", [d for d in data if isinstance(d, dict)] if isinstance(data, dict): tools = data.get("tools") if isinstance(tools, list): return str(data.get("name", "tools")), [d for d in tools if isinstance(d, dict)] if data.get("name") and (data.get("parameters") or data.get("inputSchema")): return str(data["name"]), [data] raise ToolSetError("expected a tools array, a {'tools': [...]} object, or one tool") def load_pack(path: Path) -> list[Tool]: """Load one curated pack file; the pack's declared name is each tool's domain.""" data = json.loads(path.read_text(encoding="utf-8")) domain, dicts = _extract(data) return [tool_from_dict(d, domain=domain) for d in dicts] def available_packs() -> list[Path]: """All curated packs shipped with the demo, sorted by name (deterministic order).""" return sorted(PACKS_DIR.glob("*.json")) def load_catalog() -> list[Tool]: """Every curated pack merged into one catalog, in a deterministic order.""" return [tool for path in available_packs() for tool in load_pack(path)]