""" Base classes for tool implementations. All tools inherit from BaseTool and return ToolResult for consistent handling. """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from pathlib import Path from typing import Any @dataclass class ToolResult: """ Standardized result from tool execution. All tools return this for consistent error handling and LLM consumption. """ success: bool data: Any error: str | None = None truncated: bool = False metadata: dict[str, Any] = field(default_factory=dict) def to_string(self, max_length: int = 30000) -> str: """ Convert result to string for LLM consumption. Args: max_length: Maximum string length before truncation """ if not self.success: return f"ERROR: {self.error}" if isinstance(self.data, str): content = self.data else: content = str(self.data) if len(content) > max_length: self.truncated = True content = content[:max_length] + "\n... (truncated)" if self.truncated: content += "\n[Output was truncated]" return content def __str__(self) -> str: return self.to_string() class BaseTool(ABC): """ Abstract base class for all tools. All domain tools (file, shell, search) inherit from this and implement execute(). Usage: class MyTool(BaseTool): name = "my_tool" description = "Does something useful" async def execute(self, **kwargs) -> ToolResult: return ToolResult(success=True, data="result") """ @property @abstractmethod def name(self) -> str: """Tool name for registration and identification.""" pass @property @abstractmethod def description(self) -> str: """ Tool description for LLM. Should include: - What the tool does - Arguments and their types - Return value description - Usage constraints/examples """ pass @abstractmethod async def execute(self, **kwargs: Any) -> ToolResult: """ Execute the tool with given arguments. Returns: ToolResult with success status and data or error """ pass def _validate_path(self, path: str | Path, allowed_paths: list[str]) -> bool: """ Validate that a path is within allowed directories. Args: path: Path to validate allowed_paths: List of allowed directory prefixes Returns: True if path is allowed, False otherwise """ if not allowed_paths: return True # No restrictions when allowed_paths is empty resolved = Path(path).resolve() return any( str(resolved).startswith(str(Path(allowed).resolve())) for allowed in allowed_paths ) def _error(self, message: str) -> ToolResult: """Create an error result.""" return ToolResult(success=False, data=None, error=message) def _success( self, data: Any, truncated: bool = False, **metadata: Any ) -> ToolResult: """Create a success result.""" return ToolResult( success=True, data=data, truncated=truncated, metadata=metadata )