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>
164 lines
5.3 KiB
Python
164 lines
5.3 KiB
Python
"""
|
|
Tool registrations for the Explore agent.
|
|
|
|
Registers our tool implementations with the PydanticAI agent.
|
|
"""
|
|
from pydantic_ai import Agent, RunContext
|
|
|
|
from src.domains.agents.base import AgentContext
|
|
from src.domains.tools.file.read import ReadFileTool
|
|
from src.domains.tools.file.glob import GlobFilesTool
|
|
from src.domains.tools.search.grep import GrepContentTool
|
|
from src.domains.tools.shell.bash import BashReadOnlyTool
|
|
|
|
|
|
def register_explore_tools(agent: Agent[AgentContext, str]) -> None:
|
|
"""
|
|
Register all exploration tools with the agent.
|
|
|
|
Each tool is wrapped to use context from RunContext.
|
|
"""
|
|
|
|
@agent.tool
|
|
async def read_file(
|
|
ctx: RunContext[AgentContext],
|
|
file_path: str,
|
|
offset: int = 0,
|
|
limit: int = 2000
|
|
) -> str:
|
|
"""Read contents of a file with line numbers.
|
|
|
|
Args:
|
|
file_path: Absolute path to the file to read
|
|
offset: Line number to start from (0-based, default: 0)
|
|
limit: Maximum number of lines to read (default: 2000)
|
|
|
|
Returns:
|
|
File contents with line numbers, or error message.
|
|
|
|
IMPORTANT: Always use absolute paths. Never guess file contents.
|
|
"""
|
|
tool = ReadFileTool(allowed_paths=ctx.deps.allowed_paths)
|
|
result = await tool.execute(
|
|
file_path=file_path,
|
|
offset=offset,
|
|
limit=limit
|
|
)
|
|
return result.to_string()
|
|
|
|
@agent.tool
|
|
async def glob_files(
|
|
ctx: RunContext[AgentContext],
|
|
pattern: str,
|
|
path: str | None = None,
|
|
limit: int = 100
|
|
) -> str:
|
|
"""Find files matching a glob pattern.
|
|
|
|
Args:
|
|
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
|
|
path: Directory to search in (default: working directory)
|
|
limit: Maximum number of files to return (default: 100)
|
|
|
|
Returns:
|
|
List of absolute file paths, sorted by modification time (newest first).
|
|
|
|
Examples:
|
|
- "**/*.py" finds all Python files
|
|
- "src/**/*.ts" finds TypeScript files in src/
|
|
- "**/test_*.py" finds all test files
|
|
|
|
IMPORTANT: Use this to discover files before reading them.
|
|
"""
|
|
tool = GlobFilesTool(allowed_paths=ctx.deps.allowed_paths)
|
|
search_path = path or ctx.deps.working_dir
|
|
result = await tool.execute(
|
|
pattern=pattern,
|
|
path=search_path,
|
|
limit=limit
|
|
)
|
|
return result.to_string()
|
|
|
|
@agent.tool
|
|
async def grep_content(
|
|
ctx: RunContext[AgentContext],
|
|
pattern: str,
|
|
path: str | None = None,
|
|
file_glob: str | None = None,
|
|
context_lines: int = 0,
|
|
case_sensitive: bool = True
|
|
) -> str:
|
|
"""Search file contents using regex pattern.
|
|
|
|
Args:
|
|
pattern: Regex pattern to search for (Python re syntax)
|
|
path: Directory or file to search (default: working directory)
|
|
file_glob: Filter files by glob (e.g., "*.py", "*.ts")
|
|
context_lines: Lines of context before/after matches (default: 0)
|
|
case_sensitive: Case-sensitive search (default: True)
|
|
|
|
Returns:
|
|
Matching lines with file paths and line numbers.
|
|
Format: "filepath:line_num: content"
|
|
|
|
Examples:
|
|
- pattern="def.*__init__" finds init methods
|
|
- pattern="class\\s+\\w+" finds class definitions
|
|
- pattern="TODO|FIXME" finds todo comments
|
|
|
|
IMPORTANT: Use this to search for code patterns. Escape regex special chars.
|
|
"""
|
|
tool = GrepContentTool(allowed_paths=ctx.deps.allowed_paths)
|
|
search_path = path or ctx.deps.working_dir
|
|
result = await tool.execute(
|
|
pattern=pattern,
|
|
path=search_path,
|
|
file_glob=file_glob,
|
|
context_lines=context_lines,
|
|
case_sensitive=case_sensitive
|
|
)
|
|
return result.to_string()
|
|
|
|
@agent.tool
|
|
async def bash_readonly(
|
|
ctx: RunContext[AgentContext],
|
|
command: str,
|
|
cwd: str | None = None,
|
|
timeout: int = 30
|
|
) -> str:
|
|
"""Execute a read-only bash command.
|
|
|
|
ALLOWED commands:
|
|
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
|
|
- Git (read-only): git status, git log, git diff, git show, git branch
|
|
- Text processing: grep, awk, sed (read-only), sort, uniq
|
|
- System info: pwd, whoami, hostname, which
|
|
|
|
FORBIDDEN:
|
|
- File modification (rm, mv, cp, mkdir, touch)
|
|
- Redirects (>, >>)
|
|
- Command chaining (&&, ||, ;)
|
|
- Network (curl, wget)
|
|
|
|
Args:
|
|
command: The bash command to execute
|
|
cwd: Working directory (default: agent working directory)
|
|
timeout: Timeout in seconds (default: 30)
|
|
|
|
Returns:
|
|
Command output or error message.
|
|
|
|
Examples:
|
|
- "ls -la" lists files with details
|
|
- "git status" shows git status
|
|
- "git log --oneline -10" shows recent commits
|
|
"""
|
|
tool = BashReadOnlyTool(allowed_paths=ctx.deps.allowed_paths)
|
|
working_dir = cwd or ctx.deps.working_dir
|
|
result = await tool.execute(
|
|
command=command,
|
|
cwd=working_dir,
|
|
timeout=min(timeout, ctx.deps.timeout_seconds)
|
|
)
|
|
return result.to_string()
|