feat: initial FastAPI boilerplate setup
Set up Webber - multi-agent AI development system with: - Domain-based project structure (src/domains/, src/shared/) - BaseController pattern with lazy router instantiation - Pydantic Settings configuration with env file support - Logger decorator with temporal benchmarking and trace IDs - UserProvider singleton for request-scoped context - Custom exception hierarchy - Health endpoints (/, /health) Dependencies (CVE checked 2026-01-09): - FastAPI 0.128.0, Starlette 0.50.0, Uvicorn 0.40.0 - Pydantic 2.12.4, PydanticAI 1.40.0 - All packages at latest safe versions Placeholder domains for future implementation: - agents/ (explore, plan, task) - tools/ (file, shell, search) - auth/ (tatlock integration) Port: 8086 (per CONTAINERS.md allocation) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Agents Domain
|
||||
|
||||
This domain contains PydanticAI agent definitions and orchestration.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
agents/
|
||||
├── router.py # Agent routes (list, run)
|
||||
├── controller.py # Agent orchestration logic
|
||||
├── schemas.py # Request/response models
|
||||
│
|
||||
├── explore/ # Explore agent - codebase navigation
|
||||
│ ├── agent.py # PydanticAI agent definition
|
||||
│ └── prompts.py # System prompts
|
||||
│
|
||||
├── plan/ # Plan agent - implementation design
|
||||
│ ├── agent.py
|
||||
│ └── prompts.py
|
||||
│
|
||||
└── task/ # Task agent - execution
|
||||
├── agent.py
|
||||
└── prompts.py
|
||||
```
|
||||
|
||||
## PydanticAI Pattern
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.ollama import OllamaModel
|
||||
from src.shared.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
explore_agent = Agent(
|
||||
OllamaModel(settings.ollama_agent_model, base_url=settings.ollama_url),
|
||||
system_prompt='You are a code exploration assistant...',
|
||||
)
|
||||
|
||||
@explore_agent.tool
|
||||
async def search_files(ctx, pattern: str) -> str:
|
||||
"""Search for files matching pattern."""
|
||||
# Implementation uses tools from src/domains/tools/
|
||||
pass
|
||||
```
|
||||
|
||||
## Adding a New Agent
|
||||
|
||||
1. Create a new directory under `agents/` (e.g., `agents/review/`)
|
||||
2. Create `agent.py` with PydanticAI Agent definition
|
||||
3. Create `prompts.py` with system prompts
|
||||
4. Register in `controller.py`
|
||||
5. Add tests in `tests/domains/test_agents/`
|
||||
@@ -0,0 +1,43 @@
|
||||
# Auth Domain
|
||||
|
||||
This domain handles API key management and authentication.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
auth/
|
||||
├── router.py # Auth routes
|
||||
├── controller.py # Auth logic
|
||||
└── schemas.py # Auth models
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. Client sends `X-API-Key` header
|
||||
2. Middleware validates key (via `shared/auth.py`)
|
||||
3. User context set in `shared/context.py`
|
||||
4. Routes use `Depends(require_auth)` for protected endpoints
|
||||
|
||||
## Integration with Tatlock
|
||||
|
||||
API keys are validated against the tatlock-ui/core-api user management system.
|
||||
|
||||
```python
|
||||
# In shared/auth.py
|
||||
async def validate_api_key(api_key: str) -> Optional[User]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{settings.tatlock_api_url}/auth/validate",
|
||||
headers={"X-API-Key": api_key}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return User(**response.json())
|
||||
return None
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Implement tatlock API key validation
|
||||
- [ ] Add API key generation endpoint
|
||||
- [ ] Add rate limiting per API key
|
||||
- [ ] Add usage tracking
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Health check controller.
|
||||
|
||||
Provides service health and information endpoints.
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.shared.base import BaseController
|
||||
from src.shared.config import get_settings
|
||||
from src.shared.logging import get_logger, logged
|
||||
|
||||
logger = get_logger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response."""
|
||||
status: str
|
||||
version: str
|
||||
service: str
|
||||
|
||||
|
||||
class InfoResponse(BaseModel):
|
||||
"""Service info response."""
|
||||
service: str
|
||||
version: str
|
||||
status: str
|
||||
docs: str
|
||||
debug: bool
|
||||
|
||||
|
||||
class HealthController(BaseController):
|
||||
"""Controller for health and info endpoints."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(prefix="", tags=["Health"])
|
||||
|
||||
def create_router(self) -> APIRouter:
|
||||
router = APIRouter(tags=self.tags)
|
||||
|
||||
@router.get("/", response_model=InfoResponse, summary="Service information")
|
||||
@logged()
|
||||
async def root():
|
||||
"""Get service information."""
|
||||
return InfoResponse(
|
||||
service=settings.app_name,
|
||||
version=settings.app_version,
|
||||
status="healthy",
|
||||
docs="/docs",
|
||||
debug=settings.debug,
|
||||
)
|
||||
|
||||
@router.get("/health", response_model=HealthResponse, summary="Health check")
|
||||
@logged()
|
||||
async def health_check():
|
||||
"""
|
||||
Health check endpoint.
|
||||
|
||||
Returns service health status for monitoring and load balancers.
|
||||
"""
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
version=settings.app_version,
|
||||
service=settings.app_name,
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
health_controller = HealthController()
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Health check routes.
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.domains.health.controller import health_controller
|
||||
|
||||
router = health_controller.router
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Root router - composes all domain routers.
|
||||
|
||||
Import and include domain routers here.
|
||||
main.py only includes this root_router.
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.domains.health.router import router as health_router
|
||||
# from src.domains.auth.router import router as auth_router
|
||||
# from src.domains.agents.router import router as agents_router
|
||||
# from src.domains.tools.router import router as tools_router
|
||||
|
||||
root_router = APIRouter()
|
||||
|
||||
# Health (no prefix - root level)
|
||||
root_router.include_router(health_router)
|
||||
|
||||
# Auth domain
|
||||
# root_router.include_router(auth_router, prefix="/auth", tags=["Auth"])
|
||||
|
||||
# Agents domain
|
||||
# root_router.include_router(agents_router, prefix="/agents", tags=["Agents"])
|
||||
|
||||
# Tools domain
|
||||
# root_router.include_router(tools_router, prefix="/tools", tags=["Tools"])
|
||||
@@ -0,0 +1,70 @@
|
||||
# Tools Domain
|
||||
|
||||
This domain contains tool implementations for agent use.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
tools/
|
||||
├── router.py # Tool routes (list, execute)
|
||||
├── controller.py # Tool orchestration
|
||||
├── schemas.py # Tool request/response models
|
||||
│
|
||||
├── file/ # File operation tools
|
||||
│ ├── read.py # Read file contents
|
||||
│ ├── write.py # Write file contents
|
||||
│ └── glob.py # Find files by pattern
|
||||
│
|
||||
├── shell/ # Shell execution tools
|
||||
│ └── bash.py # Execute bash commands
|
||||
│
|
||||
└── search/ # Search tools
|
||||
├── grep.py # Search file contents
|
||||
└── web.py # Web search
|
||||
```
|
||||
|
||||
## Tool Pattern
|
||||
|
||||
Tools are registered with PydanticAI agents via the `@agent.tool` decorator.
|
||||
Each tool should:
|
||||
|
||||
1. Have clear input/output types
|
||||
2. Include a docstring (used by LLM)
|
||||
3. Handle errors gracefully
|
||||
4. Respect sandbox settings
|
||||
|
||||
```python
|
||||
from src.shared.config import get_settings
|
||||
from src.shared.logging import logged
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
@logged()
|
||||
async def read_file(file_path: str, limit: int = 2000) -> str:
|
||||
"""
|
||||
Read contents of a file.
|
||||
|
||||
Args:
|
||||
file_path: Absolute path to the file
|
||||
limit: Maximum lines to read
|
||||
|
||||
Returns:
|
||||
File contents as string
|
||||
"""
|
||||
# Check path is allowed
|
||||
if settings.sandbox_enabled:
|
||||
# Validate against allowed_paths
|
||||
pass
|
||||
|
||||
# Read and return
|
||||
pass
|
||||
```
|
||||
|
||||
## Adding a New Tool
|
||||
|
||||
1. Create a new file in appropriate category (file/, shell/, search/)
|
||||
2. Implement the tool function with proper types and docstring
|
||||
3. Add `@logged()` decorator for timing
|
||||
4. Handle sandbox restrictions
|
||||
5. Register with agents that need it
|
||||
6. Add tests
|
||||
Reference in New Issue
Block a user