from __future__ import annotations from abc import ABC, abstractmethod from typing import Any from app.schema import ToolResult class BaseTool(ABC): name: str = "" description: str = "" parameters: dict[str, Any] = {} async def __call__(self, **kwargs: Any) -> ToolResult: """Execute the tool, catching exceptions and returning ToolResult. Subclasses should override execute(), not __call__. """ try: return await self.execute(**kwargs) except Exception as e: return ToolResult(error=str(e)) @abstractmethod async def execute(self, **kwargs: Any) -> ToolResult: """Execute the tool and return a ToolResult.""" def to_openai_schema(self) -> dict[str, Any]: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, }, } async def cleanup(self) -> None: """Release any held resources.""" class ToolCollection: def __init__(self, *tools: BaseTool) -> None: self._tools: dict[str, BaseTool] = {t.name: t for t in tools} def add(self, tool: BaseTool) -> None: self._tools[tool.name] = tool def get(self, name: str) -> BaseTool | None: return self._tools.get(name) async def execute(self, name: str, **kwargs: Any) -> ToolResult: tool = self._tools.get(name) if tool is None: return ToolResult(error=f"Tool '{name}' not found. Available: {list(self._tools)}") # Call execute directly — BaseTool.__call__ handles exception wrapping try: return await tool.execute(**kwargs) except Exception as e: return ToolResult(error=str(e)) def to_openai_schemas(self) -> list[dict[str, Any]]: return [t.to_openai_schema() for t in self._tools.values()] async def cleanup_all(self) -> None: for tool in self._tools.values(): try: await tool.cleanup() except Exception: pass def __iter__(self): return iter(self._tools.values()) def __len__(self): return len(self._tools)