Files
webber/webber-api/docs/architecture.md
T
jpmschweitzerandClaude Opus 4.5 acf231eb66
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 1m14s
feat: add retry logic for transient failures
- @with_retry decorator and retry_async() function
- Exponential backoff with jitter
- Retries on: timeout, connection errors, HTTP 429/5xx
- Web search tool now retries on network failures
- Configurable via RETRY_MAX_ATTEMPTS, RETRY_BASE_DELAY, RETRY_MAX_DELAY
- 29 new tests (205 total passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:22:16 +01:00

12 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)
│   │
│   ├── db/                  # Database layer
│   │   ├── __init__.py      # Exports: Database, get_database, get_session
│   │   ├── database.py      # SQLAlchemy async engine, session factory
│   │   └── models.py        # Base declarative model
│   │
│   ├── 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
│   │   └── tokens.py        # Token counting utilities (litellm)
│   │
│   └── domains/             # Feature domains
│       ├── router.py        # Root router (composes all)
│       ├── health/          # Health endpoints
│       ├── auth/            # Authentication
│       ├── conversations/   # Multi-turn conversation memory
│       │   ├── models.py    # Conversation, Message SQLAlchemy models
│       │   ├── schemas.py   # Pydantic request/response models
│       │   ├── service.py   # ConversationService business logic
│       │   ├── router.py    # REST API endpoints
│       │   └── summarize.py # Context summarization logic
│       ├── 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
DATABASE_URL sqlite+aiosqlite:///./webber.db Database connection URL
SUMMARIZATION_THRESHOLD 0.8 Summarize at N% of max tokens
SUMMARIZATION_TARGET_TOKENS 500 Target summary size
KEEP_RECENT_MESSAGES 6 Messages to keep unsummarized
RETRY_MAX_ATTEMPTS 3 Max retry attempts for transient failures
RETRY_BASE_DELAY 1.0 Base delay between retries (seconds)
RETRY_MAX_DELAY 30.0 Maximum delay between retries (seconds)

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)

Database Layer

SQLAlchemy 2.0 async with lazy initialization pattern.

Supported Databases

  • Development: SQLite via aiosqlite
  • Production: PostgreSQL via asyncpg

Pattern

from src.db import get_session
from sqlalchemy.ext.asyncio import AsyncSession

async def my_endpoint(session: AsyncSession = Depends(get_session)):
    # Session auto-commits on success, rollbacks on exception
    result = await session.execute(query)

Tables are created lazily on first get_session() call.


Conversation API

Multi-turn conversation memory with automatic context summarization.

Endpoints

Endpoint Method Description
/conversations/ POST Create new conversation
/conversations/ GET List user's conversations
/conversations/{id} GET Get conversation with history
/conversations/{id}/messages POST Add message, triggers agent
/conversations/{id} DELETE Delete conversation

Models

  • Conversation: User session with agent type, working directory
  • Message: Individual messages with role, content, token count

Context Summarization

When total tokens exceed 80% of MAX_CONTEXT_TOKENS:

  1. Keep last 6 messages intact
  2. Summarize older messages into a single summary message
  3. Mark old messages as summarized (soft delete)

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