docs: add architecture.md and remove implementation plan
Replace temporary implementation plan with permanent architecture documentation covering project structure, core patterns, and deployment details. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
# 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:
|
||||
|
||||
```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://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
|
||||
|
||||
```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.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
|
||||
|
||||
```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
|
||||
@@ -1,628 +0,0 @@
|
||||
# Webber FastAPI Boilerplate Plan
|
||||
|
||||
## Overview
|
||||
Set up FastAPI boilerplate for "Webber" - a multi-agent AI development system (similar to Claude Code, but local with different models). Follows core-api patterns with defensive coding practices.
|
||||
|
||||
**Key Decision: PydanticAI Framework**
|
||||
After research, [PydanticAI](https://ai.pydantic.dev/) is the recommended agent coordination framework:
|
||||
- Model-agnostic: supports Ollama, OpenAI, Anthropic, and 20+ providers
|
||||
- Type-safe with Pydantic validation (same ecosystem as FastAPI)
|
||||
- Built-in tool/function calling with automatic schema generation
|
||||
- Multi-agent support for complex workflows
|
||||
- Maintained by Pydantic team (285M+ monthly downloads)
|
||||
|
||||
**Port: 8086** (next available slot after Headscale 8085 per CONTAINERS.md)
|
||||
|
||||
**Default Models (always hot in VRAM on tower-of-joy):**
|
||||
- Agent reasoning: `mistral-nemo-large:latest`
|
||||
- Embeddings: `nomic-embed-text:latest`
|
||||
|
||||
**Target Clients:**
|
||||
- **Tatlock Butler**: External advisor integration for coding/software guidance
|
||||
- **CLI Interface**: TBD - command-line interface for local development
|
||||
|
||||
**Multi-tenancy:** API key authentication integrated with tatlock-ui/core-api user management
|
||||
|
||||
---
|
||||
|
||||
## 1. Directory Structure
|
||||
|
||||
```
|
||||
webber/
|
||||
├── AGENTS.md # Expanded with defensive LLM guidelines
|
||||
├── README.md # Project overview
|
||||
├── CHANGELOG.md # Version history
|
||||
├── pyproject.toml # Package metadata
|
||||
├── requirements.txt # Production dependencies only (~= pinned)
|
||||
├── requirements-dev.txt # Dev/test dependencies (pytest, pip-audit, etc.)
|
||||
├── .env.example # Environment template
|
||||
├── wakeup.sh # Dev startup (update port to 8086)
|
||||
│
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI app, lifespan, user provider init
|
||||
│ │ # NO routes here - delegates to domain routers
|
||||
│ │
|
||||
│ ├── shared/ # Cross-cutting concerns
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # BaseController, BaseSchema
|
||||
│ │ ├── config.py # Pydantic BaseSettings
|
||||
│ │ ├── logging.py # Logger decorator + centralized setup
|
||||
│ │ ├── exceptions.py # Custom exception hierarchy
|
||||
│ │ ├── auth.py # API key validation, multi-tenant support
|
||||
│ │ └── context.py # UserProvider singleton, request context
|
||||
│ │
|
||||
│ └── domains/ # Feature domains (each with router.py)
|
||||
│ ├── __init__.py
|
||||
│ ├── router.py # Root router - includes all domain routers
|
||||
│ │
|
||||
│ ├── health/ # Health endpoints
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # Health routes
|
||||
│ │ └── controller.py # Health logic
|
||||
│ │
|
||||
│ ├── auth/ # Authentication domain
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # Auth routes (API key mgmt)
|
||||
│ │ ├── controller.py
|
||||
│ │ └── schemas.py
|
||||
│ │
|
||||
│ │── agents/ # Agent domain container
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # Agent routes (lists agents, runs them)
|
||||
│ │ ├── controller.py # Agent orchestration logic
|
||||
│ │ ├── schemas.py
|
||||
│ │ │
|
||||
│ │ ├── explore/ # Explore agent (codebase navigation)
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── agent.py # PydanticAI agent definition
|
||||
│ │ │ └── prompts.py # System prompts
|
||||
│ │ │
|
||||
│ │ ├── plan/ # Plan agent (implementation design)
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── agent.py
|
||||
│ │ │ └── prompts.py
|
||||
│ │ │
|
||||
│ │ └── task/ # Task agent (execution)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── agent.py
|
||||
│ │ └── prompts.py
|
||||
│ │
|
||||
│ └── tools/ # Tool domain container
|
||||
│ ├── __init__.py
|
||||
│ ├── router.py # Tool routes (list tools, execute)
|
||||
│ ├── controller.py # Tool orchestration
|
||||
│ ├── schemas.py
|
||||
│ │
|
||||
│ ├── file/ # File operation tools
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── read.py
|
||||
│ │ ├── write.py
|
||||
│ │ └── glob.py
|
||||
│ │
|
||||
│ ├── shell/ # Shell execution tools
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── bash.py
|
||||
│ │
|
||||
│ └── search/ # Search tools
|
||||
│ ├── __init__.py
|
||||
│ ├── grep.py
|
||||
│ └── web.py
|
||||
│
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py
|
||||
│ └── test_health.py
|
||||
│
|
||||
└── docs/
|
||||
└── architecture.md
|
||||
```
|
||||
|
||||
### Key Architectural Decisions
|
||||
|
||||
1. **Clean main.py**: Only app creation, lifespan, and UserProvider init. All routes in domain routers.
|
||||
2. **Domain routers**: Each domain has `router.py` that defines routes. Root `domains/router.py` composes them.
|
||||
3. **Separate agent domains**: Each agent type (explore, plan, task) in its own subdir under `agents/`.
|
||||
4. **Separate tool domains**: Each tool category (file, shell, search) in its own subdir under `tools/`.
|
||||
5. **UserProvider singleton**: Set once in main.py lifespan, accessible everywhere via `shared/context.py`.
|
||||
6. **Multi-tenant auth**: API key validation in `shared/auth.py`, integrates with tatlock-ui/core-api.
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Files to Create
|
||||
|
||||
### Phase 1: Foundation (fully implemented)
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/shared/base.py` | BaseController, BaseSchema |
|
||||
| `src/shared/config.py` | Settings via Pydantic BaseSettings |
|
||||
| `src/shared/logging.py` | Logger decorator + centralized setup |
|
||||
| `src/shared/exceptions.py` | Custom exception hierarchy |
|
||||
| `src/shared/auth.py` | API key validation, tatlock integration stub |
|
||||
| `src/shared/context.py` | UserProvider singleton pattern |
|
||||
| `src/main.py` | FastAPI app, lifespan, UserProvider init (no routes!) |
|
||||
| `src/domains/router.py` | Root router composing all domain routers |
|
||||
| `src/domains/health/router.py` | Health routes |
|
||||
| `src/domains/health/controller.py` | Health logic |
|
||||
| `pyproject.toml` | Package metadata, pytest config |
|
||||
| `requirements.txt` | Production deps (~= pinned) |
|
||||
| `requirements-dev.txt` | Dev/test deps (pytest, pip-audit) |
|
||||
| `.env.example` | Environment variable template |
|
||||
| `tests/conftest.py` | Pytest fixtures |
|
||||
| `tests/test_health.py` | Basic endpoint tests |
|
||||
|
||||
### Phase 2: Placeholders (structure + README docs)
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/domains/auth/` | API key management (stub) |
|
||||
| `src/domains/agents/` | Agent container with explore/plan/task subdirs |
|
||||
| `src/domains/tools/` | Tool container with file/shell/search subdirs |
|
||||
| `docs/architecture.md` | System design documentation |
|
||||
|
||||
---
|
||||
|
||||
## 3. Dependency Management
|
||||
|
||||
### requirements.txt (Production - baked into Docker)
|
||||
```
|
||||
# Webber Production Dependencies
|
||||
# Minor version pinning (~=) for security patches
|
||||
# CVE check date: 2026-01-09
|
||||
# CVE check sources: PyPI, GitHub Advisories, Snyk, NVD
|
||||
|
||||
# Core FastAPI
|
||||
fastapi~=0.115.0
|
||||
starlette~=0.45.0
|
||||
uvicorn[standard]~=0.34.0
|
||||
pydantic~=2.11.0
|
||||
pydantic-settings~=2.7.0
|
||||
|
||||
# Agent Framework
|
||||
pydantic-ai~=0.0.39 # Multi-agent LLM orchestration
|
||||
|
||||
# HTTP
|
||||
httpx~=0.28.0
|
||||
aiofiles~=24.1.0
|
||||
|
||||
# Utilities
|
||||
python-multipart~=0.0.18
|
||||
python-dotenv~=1.0.0
|
||||
```
|
||||
|
||||
### requirements-dev.txt (Dev/Test only - NOT in Docker)
|
||||
```
|
||||
# Webber Development Dependencies
|
||||
# Install with: pip install -r requirements-dev.txt
|
||||
|
||||
-r requirements.txt # Include production deps
|
||||
|
||||
# Testing
|
||||
pytest~=8.3.0
|
||||
pytest-asyncio~=0.24.0
|
||||
pytest-cov~=6.0.0
|
||||
|
||||
# Security auditing
|
||||
pip-audit~=2.7.0 # Run before releases: pip-audit
|
||||
|
||||
# Type checking
|
||||
mypy~=1.13.0
|
||||
|
||||
# Code formatting (optional)
|
||||
# ruff~=0.8.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. AGENTS.md Additions
|
||||
|
||||
Add these new sections:
|
||||
|
||||
### Section 3: Defensive LLM Coding Practices
|
||||
- Input validation requirements
|
||||
- Output parsing guidelines (expect malformed responses)
|
||||
- Timeout and retry policies
|
||||
- Security: no secrets in prompts, sandbox execution
|
||||
|
||||
### Section 4: Pattern Reuse Requirements
|
||||
- Search existing code before writing new
|
||||
- Check `src/shared/` for base classes
|
||||
- Follow domain structure template
|
||||
- Code review checklist
|
||||
|
||||
### Section 5: CVE Check Process
|
||||
- Check PyPI, GitHub Advisories, Snyk, NVD before adding deps
|
||||
- Document CVE decisions in requirements.txt
|
||||
- Run `pip-audit` before releases
|
||||
|
||||
### Section 6: Mandatory Documentation
|
||||
- Required reading before work: AGENTS.md, docs/architecture.md, src/shared/base.py
|
||||
- Changelog and docstring requirements
|
||||
|
||||
### Section 7: Project Structure Reference
|
||||
- Directory tree with explanations
|
||||
- Domain structure template
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration (Settings)
|
||||
|
||||
Environment variables for:
|
||||
- **App**: DEBUG, LOG_LEVEL
|
||||
- **Server**: HOST, PORT (default **8086** per CONTAINERS.md allocation)
|
||||
- **CORS**: origins, methods, headers
|
||||
- **LLM Models** (hot in VRAM on tower-of-joy):
|
||||
- OLLAMA_URL (default: http://192.168.86.149:11434)
|
||||
- OLLAMA_AGENT_MODEL (default: mistral-nemo-large:latest)
|
||||
- OLLAMA_EMBED_MODEL (default: nomic-embed-text:latest)
|
||||
- **Auth**:
|
||||
- TATLOCK_API_URL (default: http://192.168.86.149:8000)
|
||||
- Internal API key for tatlock user validation
|
||||
- **Tools**: TOOL_TIMEOUT_SECONDS, SANDBOX_ENABLED, ALLOWED_PATHS
|
||||
- **Sessions**: SESSION_TTL_HOURS, MAX_CONTEXT_TOKENS
|
||||
|
||||
---
|
||||
|
||||
## 6. Core Patterns
|
||||
|
||||
### Logger Decorator with Temporal Benchmarking (shared/logging.py)
|
||||
```python
|
||||
import functools
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
from typing import Callable, Optional
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from uuid import uuid4
|
||||
|
||||
# Trace context for nested timing
|
||||
@dataclass
|
||||
class TraceSpan:
|
||||
name: str
|
||||
trace_id: str
|
||||
parent_id: Optional[str] = None
|
||||
span_id: str = field(default_factory=lambda: uuid4().hex[:8])
|
||||
start_time: float = field(default_factory=time.perf_counter)
|
||||
end_time: Optional[float] = None
|
||||
|
||||
@property
|
||||
def duration_ms(self) -> float:
|
||||
if self.end_time is None:
|
||||
return (time.perf_counter() - self.start_time) * 1000
|
||||
return (self.end_time - self.start_time) * 1000
|
||||
|
||||
# Context variable for trace propagation
|
||||
_current_span: ContextVar[Optional[TraceSpan]] = ContextVar('current_span', default=None)
|
||||
_trace_id: ContextVar[Optional[str]] = ContextVar('trace_id', default=None)
|
||||
|
||||
def get_current_trace_id() -> Optional[str]:
|
||||
"""Get current trace ID for correlation."""
|
||||
return _trace_id.get()
|
||||
|
||||
def logged(
|
||||
logger: logging.Logger = None,
|
||||
slow_threshold_ms: float = 100.0,
|
||||
warn_threshold_ms: float = 500.0,
|
||||
include_args: bool = False,
|
||||
):
|
||||
"""
|
||||
Decorator for automatic function logging with temporal benchmarking.
|
||||
|
||||
Args:
|
||||
logger: Logger instance (defaults to module logger)
|
||||
slow_threshold_ms: Log INFO if execution exceeds this (default 100ms)
|
||||
warn_threshold_ms: Log WARNING if execution exceeds this (default 500ms)
|
||||
include_args: Include function arguments in log (careful with sensitive data)
|
||||
|
||||
Usage:
|
||||
@logged()
|
||||
async def my_function(): ...
|
||||
|
||||
@logged(slow_threshold_ms=50, warn_threshold_ms=200)
|
||||
def critical_path(): ...
|
||||
"""
|
||||
def decorator(func: Callable):
|
||||
nonlocal logger
|
||||
if logger is None:
|
||||
logger = logging.getLogger(func.__module__)
|
||||
|
||||
func_name = f"{func.__module__}.{func.__qualname__}"
|
||||
|
||||
def _create_span() -> TraceSpan:
|
||||
parent = _current_span.get()
|
||||
trace_id = _trace_id.get() or uuid4().hex[:16]
|
||||
if _trace_id.get() is None:
|
||||
_trace_id.set(trace_id)
|
||||
return TraceSpan(
|
||||
name=func_name,
|
||||
trace_id=trace_id,
|
||||
parent_id=parent.span_id if parent else None,
|
||||
)
|
||||
|
||||
def _log_completion(span: TraceSpan, error: Exception = None):
|
||||
span.end_time = time.perf_counter()
|
||||
duration = span.duration_ms
|
||||
|
||||
# Build log context
|
||||
ctx = {
|
||||
"trace_id": span.trace_id,
|
||||
"span_id": span.span_id,
|
||||
"duration_ms": round(duration, 2),
|
||||
"func": func_name,
|
||||
}
|
||||
if span.parent_id:
|
||||
ctx["parent_id"] = span.parent_id
|
||||
|
||||
if error:
|
||||
logger.error(
|
||||
f"[{span.trace_id[:8]}] {func_name} FAILED after {duration:.2f}ms: {error}",
|
||||
extra=ctx,
|
||||
exc_info=True
|
||||
)
|
||||
elif duration >= warn_threshold_ms:
|
||||
logger.warning(
|
||||
f"[{span.trace_id[:8]}] {func_name} SLOW: {duration:.2f}ms (threshold: {warn_threshold_ms}ms)",
|
||||
extra=ctx
|
||||
)
|
||||
elif duration >= slow_threshold_ms:
|
||||
logger.info(
|
||||
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
|
||||
extra=ctx
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
|
||||
extra=ctx
|
||||
)
|
||||
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
span = _create_span()
|
||||
token = _current_span.set(span)
|
||||
|
||||
if include_args:
|
||||
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
|
||||
else:
|
||||
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
_log_completion(span)
|
||||
return result
|
||||
except Exception as e:
|
||||
_log_completion(span, error=e)
|
||||
raise
|
||||
finally:
|
||||
_current_span.reset(token)
|
||||
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
span = _create_span()
|
||||
token = _current_span.set(span)
|
||||
|
||||
if include_args:
|
||||
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
|
||||
else:
|
||||
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
_log_completion(span)
|
||||
return result
|
||||
except Exception as e:
|
||||
_log_completion(span, error=e)
|
||||
raise
|
||||
finally:
|
||||
_current_span.reset(token)
|
||||
|
||||
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
# Convenience for manual span creation (context manager)
|
||||
class trace_span:
|
||||
"""
|
||||
Context manager for manual span creation.
|
||||
|
||||
Usage:
|
||||
with trace_span("database_query"):
|
||||
result = await db.execute(query)
|
||||
|
||||
async with trace_span("llm_call"):
|
||||
response = await agent.run(prompt)
|
||||
"""
|
||||
def __init__(self, name: str, logger: logging.Logger = None):
|
||||
self.name = name
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.span: Optional[TraceSpan] = None
|
||||
self.token = None
|
||||
|
||||
def __enter__(self):
|
||||
parent = _current_span.get()
|
||||
trace_id = _trace_id.get() or uuid4().hex[:16]
|
||||
if _trace_id.get() is None:
|
||||
_trace_id.set(trace_id)
|
||||
|
||||
self.span = TraceSpan(
|
||||
name=self.name,
|
||||
trace_id=trace_id,
|
||||
parent_id=parent.span_id if parent else None,
|
||||
)
|
||||
self.token = _current_span.set(self.span)
|
||||
self.logger.debug(f"[{self.span.trace_id[:8]}] -> {self.name}")
|
||||
return self.span
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.span:
|
||||
self.span.end_time = time.perf_counter()
|
||||
duration = self.span.duration_ms
|
||||
if exc_val:
|
||||
self.logger.error(f"[{self.span.trace_id[:8]}] {self.name} FAILED: {duration:.2f}ms")
|
||||
else:
|
||||
self.logger.debug(f"[{self.span.trace_id[:8]}] {self.name}: {duration:.2f}ms")
|
||||
if self.token:
|
||||
_current_span.reset(self.token)
|
||||
return False
|
||||
|
||||
async def __aenter__(self):
|
||||
return self.__enter__()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
return self.__exit__(exc_type, exc_val, exc_tb)
|
||||
```
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
DEBUG [a1b2c3d4] -> src.domains.agents.controller.run_agent
|
||||
DEBUG [a1b2c3d4] -> src.domains.llm.service.call_ollama
|
||||
DEBUG [a1b2c3d4] src.domains.llm.service.call_ollama: 45.23ms
|
||||
INFO [a1b2c3d4] src.domains.agents.controller.run_agent completed in 156.78ms
|
||||
WARN [a1b2c3d4] src.domains.tools.file.read.read_file SLOW: 523.45ms (threshold: 500ms)
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- **Trace IDs**: Correlate logs across nested calls
|
||||
- **Parent/child spans**: Track call hierarchy
|
||||
- **Configurable thresholds**: `slow_threshold_ms` (INFO), `warn_threshold_ms` (WARNING)
|
||||
- **Context manager**: `trace_span()` for manual instrumentation of code blocks
|
||||
- **Zero overhead path**: Fast path for sub-threshold calls (DEBUG only)
|
||||
|
||||
### UserProvider Singleton (shared/context.py)
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from contextvars import ContextVar
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: str
|
||||
email: str
|
||||
api_key: str
|
||||
tenant_id: Optional[str] = None
|
||||
|
||||
# Context variable for request-scoped user
|
||||
_current_user: ContextVar[Optional[User]] = ContextVar('current_user', default=None)
|
||||
|
||||
class UserProvider:
|
||||
"""Singleton for user context management."""
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def set_user(self, user: User) -> None:
|
||||
_current_user.set(user)
|
||||
|
||||
def get_user(self) -> Optional[User]:
|
||||
return _current_user.get()
|
||||
|
||||
def clear_user(self) -> None:
|
||||
_current_user.set(None)
|
||||
|
||||
# Global singleton
|
||||
user_provider = UserProvider()
|
||||
```
|
||||
|
||||
### BaseController (from core-api)
|
||||
```python
|
||||
class BaseController(ABC):
|
||||
def __init__(self, prefix: str, tags: list[str]):
|
||||
self.prefix = prefix
|
||||
self.tags = tags
|
||||
self._router = None
|
||||
|
||||
@abstractmethod
|
||||
def create_router(self) -> APIRouter: pass
|
||||
|
||||
@property
|
||||
def router(self) -> APIRouter:
|
||||
if self._router is None:
|
||||
self._router = self.create_router()
|
||||
return self._router
|
||||
```
|
||||
|
||||
### PydanticAI Agent Pattern (placeholder for future)
|
||||
```python
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.ollama import OllamaModel
|
||||
|
||||
# Use the hot model from VRAM
|
||||
agent = Agent(
|
||||
OllamaModel('mistral-nemo-large:latest'),
|
||||
system_prompt='You are a helpful assistant.',
|
||||
)
|
||||
|
||||
@agent.tool
|
||||
async def search_files(ctx, pattern: str) -> str:
|
||||
"""Search for files matching pattern."""
|
||||
pass # Implementation in tools/search/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Order
|
||||
|
||||
1. **Create directory structure** (`src/`, `src/shared/`, `src/domains/`)
|
||||
2. **Implement shared modules** (base.py, config.py, logging.py, exceptions.py)
|
||||
3. **Create main.py** with FastAPI app and lifespan
|
||||
4. **Add health domain** as working example
|
||||
5. **Set up tests** (conftest.py, test_health.py)
|
||||
6. **Create placeholder domains** (llm, agents, tools - structure only)
|
||||
7. **Update AGENTS.md** with new sections
|
||||
8. **Create supporting files** (pyproject.toml, requirements.txt, .env.example)
|
||||
9. **Add docs/architecture.md**
|
||||
|
||||
---
|
||||
|
||||
## 8. Verification
|
||||
|
||||
After implementation:
|
||||
1. `./wakeup.sh` starts server without errors
|
||||
2. `curl http://localhost:8086/health` returns healthy
|
||||
3. `http://localhost:8086/docs` shows API documentation
|
||||
4. `.venv/bin/python -m pytest tests/ -v` passes
|
||||
5. Code follows patterns in AGENTS.md
|
||||
|
||||
---
|
||||
|
||||
## 9. Critical Reference Files
|
||||
|
||||
- `/mnt/media/Projects/core-api/src/shared/base.py` - BaseController pattern
|
||||
- `/mnt/media/Projects/core-api/src/shared/config.py` - Settings pattern
|
||||
- `/mnt/media/Projects/core-api/src/domains/health/controller.py` - Controller example
|
||||
- https://ai.pydantic.dev/ - PydanticAI documentation
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**What will be created:**
|
||||
- Complete FastAPI project structure following core-api patterns
|
||||
- Working health endpoint at `http://localhost:8086/health`
|
||||
- **Clean main.py** - no routes, just app init and UserProvider setup
|
||||
- **Domain routers** - each domain has router.py, composed by root router
|
||||
- **Logger decorator** - centralized logging via `@logged` decorator
|
||||
- **UserProvider singleton** - request-scoped user context, no parameter passing
|
||||
- **Multi-tenant auth stub** - API key validation ready for tatlock integration
|
||||
- Separate **requirements.txt** (prod) and **requirements-dev.txt** (dev/test)
|
||||
- Placeholder domains with agent/tool subdirectories (explore, plan, task / file, shell, search)
|
||||
- Comprehensive AGENTS.md with defensive LLM coding practices, CVE checks, pattern reuse
|
||||
- Test infrastructure with pytest
|
||||
- docs/architecture.md explaining the system design
|
||||
|
||||
**What will NOT be created (deferred):**
|
||||
- Database layer (add when needed)
|
||||
- Full agent/tool implementations (PydanticAI patterns documented for future work)
|
||||
- Docker/deployment configuration (can add later)
|
||||
- CLI interface (TBD)
|
||||
|
||||
**Key decisions:**
|
||||
- Port: **8086**
|
||||
- Agent framework: **PydanticAI**
|
||||
- Default model: **mistral-nemo-large:latest** (hot in VRAM)
|
||||
- Embeddings: **nomic-embed-text:latest** (hot in VRAM)
|
||||
- No database initially
|
||||
- Separate prod/dev requirements
|
||||
- UserProvider singleton pattern for multi-tenancy
|
||||
Reference in New Issue
Block a user