| from __future__ import annotations |
|
|
| import json |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Any |
|
|
|
|
| class RegistryError(RuntimeError): |
| pass |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class ToolSpec: |
| id: str |
| name: str |
| description: str |
| category: str |
| entry_function: str |
| arguments: tuple[str, ...] = () |
| required_arguments: tuple[str, ...] = () |
| capabilities: tuple[str, ...] = () |
| model_trainers: tuple[str, ...] = () |
| generation_options: dict[str, Any] = field(default_factory=dict) |
| requires_confirmation: bool = False |
| enabled: bool = True |
| demo: bool = False |
| backend: dict[str, Any] = field(default_factory=dict) |
|
|
| @classmethod |
| def from_dict(cls, data: dict[str, Any]) -> "ToolSpec": |
| required = {"id", "name", "description", "category", "backend"} |
| missing = required - data.keys() |
| if missing: |
| raise RegistryError(f"Tool entry is missing: {', '.join(sorted(missing))}") |
| return cls( |
| id=str(data["id"]), |
| name=str(data["name"]), |
| description=str(data["description"]), |
| category=str(data["category"]), |
| entry_function=str(data.get("entry_function", "")), |
| arguments=tuple(str(item) for item in data.get("arguments", [])), |
| required_arguments=tuple( |
| str(item) for item in data.get("required_arguments", []) |
| ), |
| capabilities=tuple(str(item) for item in data.get("capabilities", [])), |
| model_trainers=tuple( |
| str(item) for item in data.get("model_trainers", []) |
| ), |
| generation_options=dict(data.get("generation_options", {})), |
| requires_confirmation=bool(data.get("requires_confirmation", False)), |
| enabled=bool(data.get("enabled", True)), |
| demo=bool(data.get("demo", False)), |
| backend=dict(data["backend"]), |
| ) |
|
|
|
|
| class ToolRegistry: |
| def __init__(self, root: Path) -> None: |
| self.root = root.resolve() |
| self.path = self.root / "config" / "tools.json" |
| self._tools: dict[str, ToolSpec] = {} |
| self.load() |
|
|
| def load(self) -> None: |
| try: |
| payload = json.loads(self.path.read_text(encoding="utf-8")) |
| except FileNotFoundError as exc: |
| raise RegistryError(f"Tool registry not found: {self.path}") from exc |
| except json.JSONDecodeError as exc: |
| raise RegistryError(f"Tool registry is invalid JSON: {exc}") from exc |
|
|
| entries = payload.get("tools") |
| if not isinstance(entries, list): |
| raise RegistryError("Tool registry must contain a 'tools' list.") |
|
|
| loaded: dict[str, ToolSpec] = {} |
| for entry in entries: |
| spec = ToolSpec.from_dict(entry) |
| if spec.id in loaded: |
| raise RegistryError(f"Duplicate tool id: {spec.id}") |
| loaded[spec.id] = spec |
| external_path = self.root / "config" / "external_tools.json" |
| try: |
| external_payload = json.loads(external_path.read_text(encoding="utf-8")) |
| external_entries = external_payload.get("tools", []) |
| except FileNotFoundError: |
| external_entries = [] |
| except json.JSONDecodeError as exc: |
| raise RegistryError(f"External tool registry is invalid JSON: {exc}") from exc |
| if not isinstance(external_entries, list): |
| raise RegistryError("External tool registry must contain a 'tools' list.") |
| for entry in external_entries: |
| if not isinstance(entry, dict): |
| raise RegistryError("External tool entry must be an object.") |
| safe_entry = dict(entry) |
| tool_id = str(safe_entry.get("id", "")) |
| backend = dict(safe_entry.get("backend", {})) |
| path = Path(str(backend.get("path", ""))).expanduser() |
| root = Path(str(backend.get("root", ""))).expanduser() |
| if not tool_id.startswith("external_"): |
| raise RegistryError("External tool ids must start with 'external_'.") |
| if tool_id in loaded: |
| raise RegistryError(f"External tool cannot replace registered tool: {tool_id}") |
| if backend.get("type") != "script" or not path.is_absolute() or not root.is_absolute(): |
| raise RegistryError(f"External tool {tool_id} has an invalid script backend.") |
| try: |
| path.resolve().relative_to(root.resolve()) |
| except ValueError as exc: |
| raise RegistryError(f"External tool {tool_id} script is outside its folder.") from exc |
| safe_entry["requires_confirmation"] = True |
| safe_entry["demo"] = False |
| spec = ToolSpec.from_dict(safe_entry) |
| loaded[spec.id] = spec |
| self._tools = loaded |
|
|
| def get(self, tool_id: str, *, require_enabled: bool = True) -> ToolSpec: |
| try: |
| tool = self._tools[tool_id] |
| except KeyError as exc: |
| raise RegistryError(f"Unregistered tool: {tool_id}") from exc |
| if require_enabled and not tool.enabled: |
| raise RegistryError(f"Tool is not configured: {tool.name}") |
| return tool |
|
|
| def all(self) -> list[ToolSpec]: |
| return list(self._tools.values()) |
|
|
| def enabled(self) -> list[ToolSpec]: |
| return [tool for tool in self._tools.values() if tool.enabled] |
|
|
| def safe_llm_catalog(self) -> list[dict[str, Any]]: |
| return [ |
| { |
| "id": tool.id, |
| "name": tool.name, |
| "description": tool.description, |
| "arguments": list(tool.arguments), |
| "required_arguments": list(tool.required_arguments), |
| "capabilities": list(tool.capabilities), |
| "model_trainers": list(tool.model_trainers), |
| "generation_options": dict(tool.generation_options), |
| "requires_confirmation": tool.requires_confirmation, |
| } |
| for tool in self.enabled() |
| ] |
|
|