418 lines
12 KiB
Markdown
418 lines
12 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
|
|
```dockerfile
|
|
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.net/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
|
|
|
|
```bash
|
|
# 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
|