"""BaseTool ABC — foundation for all agent-callable tools.""" import logging from abc import ABC, abstractmethod from pathlib import Path from typing import Any, ClassVar from pydantic import BaseModel, ValidationError from app.models.config import AppConfig from app.models.tool_call import ToolResult, ToolResultStatus logger = logging.getLogger(__name__) class BaseTool(ABC): """Abstract base class for all agent tools. Subclasses must set the class-level ``name``, ``description``, and ``params_model`` attributes and implement ``execute``. """ name: ClassVar[str] description: ClassVar[str] params_model: ClassVar[type[BaseModel]] def __init__(self, workspace_root: Path, config: AppConfig) -> None: self.workspace_root = workspace_root self.config = config self.logger = logging.getLogger(f"{__name__}.{self.name}") @abstractmethod def execute(self, *, tool_call_id: str, **kwargs: Any) -> ToolResult: """Execute the tool with validated parameters. Subclasses implement the actual tool logic here. """ def run(self, tool_call_id: str, arguments: dict[str, Any]) -> ToolResult: """Public entry point: validate arguments, execute, guarantee a ToolResult. Never raises — all exceptions are caught and returned as error results. """ try: validated = self.params_model(**arguments) except ValidationError as exc: self.logger.warning("Validation error for %s: %s", self.name, exc) return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error=f"Invalid arguments: {exc}", ) try: return self.execute(tool_call_id=tool_call_id, **validated.model_dump()) except Exception as exc: self.logger.exception("Unexpected error in tool %s", self.name) return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error=f"Tool execution failed: {exc}", ) def get_openai_schema(self) -> dict[str, Any]: """Return the OpenAI function-calling schema for this tool.""" schema = self.params_model.model_json_schema() # Remove the top-level title/description that Pydantic adds — # those belong on the function object, not the parameters. schema.pop("title", None) schema.pop("description", None) return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": schema, }, }