Files
webber/webber-api/src/domains/tools/base.py
T
jpmschweitzerandClaude Opus 4.5 3b58fa4f8b
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 2m27s
refactor: reorganize into monorepo with separate subprojects
Structure webber into three independent subprojects:
- webber-api/: FastAPI backend server with all agent code
- webber-cli/: Standalone CLI client (renamed from cli/ to webber_cli/)
- webber-sandbox/: Test project for functional testing

Key changes:
- Each subproject has its own .venv (Python 3.12+)
- Added sandbox.sh for managing test project templates
- Created sandbox-templates/ with calculator-cli and empty starter
- Updated CI/CD for prefixed tags (api/v*, cli/v*)
- Added comprehensive AGENTS.md with operational instructions
- Added gitignore filtering to glob and grep tools
- Created pyproject.toml for each subproject

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:37:47 +01:00

135 lines
3.4 KiB
Python

"""
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
)