The homelab is retiring *.schweitz.internal and will rebind host ports to 127.0.0.1, so container-to-container traffic must use container names on the docker-dataplane network. Switch defaults from host IP:port to http://tatlock:8000 and http://searxng:8080 (SearXNG's internal port is 8080; 8087 is only the host-published port). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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://tatlock:8000 | Tatlock auth service |
| SEARXNG_URL | http://searxng:8080 | SearXNG web search instance |
| SEARXNG_TIMEOUT | 10 | SearXNG request timeout (seconds) |
| 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:
- Keep last 6 messages intact
- Summarize older messages into a single summary message
- Mark old messages as summarized (soft delete)
Authentication Flow
- Client sends
X-API-Keyheader - Auth middleware calls
validate_api_key() - Tatlock validates key and returns user info
- UserProvider stores user in request context
- Routes access via
get_current_user()orrequire_user() - Middleware clears user in
finallyblock
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:
- Push tag
v*triggers build - Creates Gitea release
- Builds and pushes Docker image to registry
- 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
- Create domain directory under
src/domains/ - Add
router.pywith routes - Add
controller.pywith business logic - Add
schemas.pyfor request/response models - Import and include router in
src/domains/router.py - Add tests in
tests/test_<domain>.py
Adding New Agents
- Create agent directory under
src/domains/agents/ - Add
agent.pywith PydanticAI agent definition - Add
prompts.pywith system prompts - Register in agents controller
- Document in
src/domains/agents/README.md
Adding New Tools
- Create tool file under appropriate
src/domains/tools/subdir - Implement tool function with type hints
- Register as PydanticAI tool
- 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