From 34b68621c6e688d08727fd6953ccf608984915b1 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 9 Jan 2026 22:05:16 +0100 Subject: [PATCH] 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 --- docs/architecture.md | 349 +++++++++++++++++++++++ implementation-plan.md | 628 ----------------------------------------- 2 files changed, 349 insertions(+), 628 deletions(-) create mode 100644 docs/architecture.md delete mode 100644 implementation-plan.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e19f268 --- /dev/null +++ b/docs/architecture.md @@ -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_.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 diff --git a/implementation-plan.md b/implementation-plan.md deleted file mode 100644 index b36e9e9..0000000 --- a/implementation-plan.md +++ /dev/null @@ -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