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,167 @@
|
||||
"""
|
||||
Base classes and registry for agent implementations.
|
||||
|
||||
All agents are built on PydanticAI and registered in a central registry.
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from pydantic_ai import Agent
|
||||
|
||||
from src.shared.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentContext:
|
||||
"""
|
||||
Base context passed to all agent tools.
|
||||
|
||||
Subclass this for agent-specific context (e.g., ExploreContext).
|
||||
"""
|
||||
working_dir: str
|
||||
allowed_paths: list[str] = field(default_factory=list)
|
||||
timeout_seconds: int = 120
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AgentProtocol(Protocol):
|
||||
"""Protocol that all agents must implement."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Unique identifier for the agent."""
|
||||
...
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Human-readable description of what the agent does."""
|
||||
...
|
||||
|
||||
@property
|
||||
def agent(self) -> Agent:
|
||||
"""The underlying PydanticAI agent."""
|
||||
...
|
||||
|
||||
async def run(self, prompt: str, **kwargs: Any) -> str:
|
||||
"""
|
||||
Execute the agent with a prompt.
|
||||
|
||||
Args:
|
||||
prompt: User prompt/query
|
||||
**kwargs: Additional arguments (working_dir, etc.)
|
||||
|
||||
Returns:
|
||||
Agent response as string
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
"""
|
||||
Abstract base class for agent implementations.
|
||||
|
||||
Provides common functionality and enforces interface.
|
||||
|
||||
Usage:
|
||||
class ExploreAgent(BaseAgent):
|
||||
name = "explore"
|
||||
description = "Fast codebase exploration"
|
||||
|
||||
def _create_agent(self) -> Agent:
|
||||
# Create and configure PydanticAI agent
|
||||
...
|
||||
|
||||
async def run(self, prompt: str, **kwargs) -> str:
|
||||
# Execute agent
|
||||
...
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Unique identifier for the agent."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def description(self) -> str:
|
||||
"""Human-readable description."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def agent(self) -> Agent:
|
||||
"""Lazy-loaded PydanticAI agent."""
|
||||
if not hasattr(self, '_agent') or self._agent is None:
|
||||
self._agent = self._create_agent()
|
||||
return self._agent
|
||||
|
||||
@abstractmethod
|
||||
def _create_agent(self) -> Agent:
|
||||
"""
|
||||
Create and configure the PydanticAI agent.
|
||||
|
||||
Override this to set up model, system prompt, and tools.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, prompt: str, **kwargs: Any) -> str:
|
||||
"""Execute the agent."""
|
||||
pass
|
||||
|
||||
|
||||
# === Agent Registry ===
|
||||
|
||||
_AGENT_REGISTRY: dict[str, BaseAgent] = {}
|
||||
|
||||
|
||||
def register_agent(agent: BaseAgent) -> BaseAgent:
|
||||
"""
|
||||
Register an agent in the global registry.
|
||||
|
||||
Args:
|
||||
agent: Agent instance to register
|
||||
|
||||
Returns:
|
||||
The registered agent (for decorator chaining)
|
||||
"""
|
||||
if agent.name in _AGENT_REGISTRY:
|
||||
logger.warning(f"Overwriting existing agent: {agent.name}")
|
||||
|
||||
_AGENT_REGISTRY[agent.name] = agent
|
||||
logger.info(f"Registered agent: {agent.name}")
|
||||
return agent
|
||||
|
||||
|
||||
def get_agent(name: str) -> BaseAgent | None:
|
||||
"""
|
||||
Get an agent by name.
|
||||
|
||||
Args:
|
||||
name: Agent name
|
||||
|
||||
Returns:
|
||||
Agent instance or None if not found
|
||||
"""
|
||||
return _AGENT_REGISTRY.get(name)
|
||||
|
||||
|
||||
def list_agents() -> list[dict[str, str]]:
|
||||
"""
|
||||
List all registered agents.
|
||||
|
||||
Returns:
|
||||
List of agent info dicts with name and description
|
||||
"""
|
||||
return [
|
||||
{"name": agent.name, "description": agent.description}
|
||||
for agent in _AGENT_REGISTRY.values()
|
||||
]
|
||||
|
||||
|
||||
def get_registry() -> dict[str, BaseAgent]:
|
||||
"""Get the full agent registry."""
|
||||
return _AGENT_REGISTRY.copy()
|
||||
Reference in New Issue
Block a user