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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user