Files
webber/webber-api/docs/architecture.md
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

9.1 KiB

Webber Architecture

Multi-Agent AI Development System - similar to Claude Code but running locally with configurable models.

Overview

Webber is a FastAPI-based agent orchestration service that provides:

  • Multi-agent execution (Explore, Plan, Task)
  • Tool capabilities (file operations, shell, search)
  • Multi-tenant authentication via Tatlock integration
  • PydanticAI framework for LLM orchestration

Port: 8086 Runtime: Python 3.12, FastAPI, Uvicorn Agent Framework: PydanticAI Default LLM: Ollama with mistral-nemo-large:latest


Directory Structure

webber/
├── src/
│   ├── main.py              # App entry point (NO routes)
│   │
│   ├── shared/              # Cross-cutting concerns
│   │   ├── base.py          # BaseController, BaseSchema
│   │   ├── config.py        # Pydantic Settings
│   │   ├── logging.py       # @logged decorator, trace_span
│   │   ├── exceptions.py    # Custom exception hierarchy
│   │   ├── auth.py          # API key validation
│   │   └── context.py       # UserProvider singleton
│   │
│   └── domains/             # Feature domains
│       ├── router.py        # Root router (composes all)
│       ├── health/          # Health endpoints
│       ├── auth/            # Authentication
│       ├── agents/          # Agent orchestration
│       │   ├── explore/     # Codebase navigation
│       │   ├── plan/        # Implementation design
│       │   └── task/        # Execution
│       └── tools/           # Tool execution
│           ├── file/        # Read, write, glob
│           ├── shell/       # Bash execution
│           └── search/      # Grep, web search
│
├── tests/
├── docs/
└── logs/

Core Patterns

1. Clean main.py

The entry point contains ONLY:

  • FastAPI app creation with lifespan
  • CORS middleware
  • Auth middleware (sets UserProvider)
  • Exception handlers
  • Single router include (root_router)

All routes live in domain routers. This keeps main.py focused on app initialization.

2. Domain-Based Structure

Each feature domain has its own directory:

domains/
├── router.py           # Root router composing all domains
├── health/
│   ├── router.py       # Domain routes
│   └── controller.py   # Business logic
├── agents/
│   ├── router.py       # Agent routes
│   ├── controller.py   # Orchestration logic
│   ├── schemas.py      # Request/response models
│   └── explore/        # Agent implementation
│       ├── agent.py    # PydanticAI agent
│       └── prompts.py  # System prompts

3. BaseController Pattern

Controllers use lazy router instantiation:

from src.shared.base import BaseController

class MyController(BaseController):
    def __init__(self):
        super().__init__(prefix="/my", tags=["My"])

    def create_router(self) -> APIRouter:
        router = APIRouter(prefix=self.prefix, tags=self.tags)

        @router.get("/")
        async def list_items():
            return []

        return router

my_controller = MyController()
# Use: my_controller.router

4. UserProvider Singleton

Request-scoped user context without parameter passing:

# In middleware (main.py):
user = await validate_api_key(api_key)
user_provider.set_user(user)
try:
    response = await call_next(request)
finally:
    user_provider.clear_user()

# Anywhere in code:
from src.shared.context import get_current_user, require_user

user = get_current_user()  # Returns None if not authenticated
user = require_user()      # Raises if not authenticated

Uses Python's contextvars for async-safe request isolation.

5. Logger with Temporal Benchmarking

The @logged() decorator automatically tracks execution time:

from src.shared.logging import logged, trace_span, get_logger

logger = get_logger(__name__)

@logged()
async def my_function():
    # Automatically logs entry, exit, and duration
    pass

@logged(slow_threshold_ms=50, warn_threshold_ms=200)
def critical_path():
    # Custom thresholds for performance-critical code
    pass

async def complex_operation():
    async with trace_span("llm_call"):
        # Manual span for specific sections
        result = await agent.run(prompt)

Features:

  • Trace ID correlation across nested calls
  • Configurable slow/warn thresholds
  • DEBUG: all calls logged with timing
  • INFO: slow calls (>100ms default)
  • WARNING: very slow calls (>500ms default)
  • ERROR: failed calls with stack trace

6. Exception Hierarchy

from src.shared.exceptions import (
    AppException,
    NotFoundError,
    ValidationError,
    AuthenticationError,
    AuthorizationError,
)

# Raise with context:
raise NotFoundError("User", user_id)
raise ValidationError("email", "Invalid format")

# Automatic JSON response via exception handlers in main.py

Configuration

All settings via environment variables or .env:

Variable Default Description
DEBUG false Enable debug mode
LOG_LEVEL INFO Logging level
HOST 0.0.0.0 Server host
PORT 8086 Server port
OLLAMA_URL http://192.168.86.149:11434 Ollama API URL
OLLAMA_AGENT_MODEL mistral-nemo-large:latest Agent reasoning model
OLLAMA_EMBED_MODEL nomic-embed-text:latest Embedding model
TATLOCK_API_URL http://192.168.86.149:8000 Tatlock auth service
TOOL_TIMEOUT_SECONDS 120 Tool execution timeout
SANDBOX_ENABLED true Enable sandboxed execution
ALLOWED_PATHS [] Paths accessible to tools
SESSION_TTL_HOURS 24 Session expiry
MAX_CONTEXT_TOKENS 128000 Max context window

Agent Architecture

Webber uses PydanticAI for agent orchestration. Each agent type is purpose-built:

Explore Agent

Fast codebase exploration for:

  • Finding files by pattern
  • Searching code for keywords
  • Answering questions about structure

Plan Agent

Implementation design for:

  • Analyzing requirements
  • Creating step-by-step plans
  • Identifying files to modify
  • Considering trade-offs

Task Agent

Autonomous execution for:

  • Multi-step implementations
  • Tool orchestration
  • Code generation and modification

Tool Architecture

Tools are sandboxed operations agents can invoke:

File Tools

  • Read: Read file contents with line limits
  • Write: Create or overwrite files
  • Edit: String replacement in files
  • Glob: Pattern-based file search

Shell Tools

  • Bash: Command execution with timeout
  • Sandboxed to allowed paths
  • Captures stdout/stderr

Search Tools

  • Grep: Regex content search via ripgrep
  • WebSearch: Web search integration (optional)

Authentication Flow

  1. Client sends X-API-Key header
  2. Auth middleware calls validate_api_key()
  3. Tatlock validates key and returns user info
  4. UserProvider stores user in request context
  5. Routes access via get_current_user() or require_user()
  6. Middleware clears user in finally block

Deployment

Docker

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml .
COPY src/ ./src/
ENV PYTHONPATH=/app
EXPOSE 8086
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8086/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8086"]

CI/CD

Gitea Actions workflow:

  1. Push tag v* triggers build
  2. Creates Gitea release
  3. Builds and pushes Docker image to registry
  4. Watchtower auto-deploys to production

Production Stack

Deployed in Portainer agents stack alongside Tatlock:

  • Network: docker-dataplane
  • Registry: git.schweitz.internal/jpmschweitzer/webber
  • Auto-update: Watchtower with label com.centurylinklabs.watchtower.enable=true

Adding New Domains

  1. Create domain directory under src/domains/
  2. Add router.py with routes
  3. Add controller.py with business logic
  4. Add schemas.py for request/response models
  5. Import and include router in src/domains/router.py
  6. Add tests in tests/test_<domain>.py

Adding New Agents

  1. Create agent directory under src/domains/agents/
  2. Add agent.py with PydanticAI agent definition
  3. Add prompts.py with system prompts
  4. Register in agents controller
  5. Document in src/domains/agents/README.md

Adding New Tools

  1. Create tool file under appropriate src/domains/tools/ subdir
  2. Implement tool function with type hints
  3. Register as PydanticAI tool
  4. Document in src/domains/tools/README.md

Testing

# Run all tests
./wakeup.sh  # Start server first
pytest tests/ -v

# With coverage
pytest tests/ --cov=src --cov-report=html

Security Considerations

  • All tool execution is sandboxed when SANDBOX_ENABLED=true
  • File operations restricted to ALLOWED_PATHS
  • No secrets in prompts
  • Input validation via Pydantic
  • Output parsing expects malformed LLM responses
  • Timeouts on all tool execution