feat(service): add FastAPI agent service with 5 routers and Docker setup
- config.py: pydantic-settings with all env vars, privacy mode, per-agent overrides - app_state.py: global singletons (pool, master agent, registry, llm_router, sweep) - main.py: FastAPI lifespan startup — DB pool, LLM router, Odoo client, agents, master - routers/dispatch.py: POST /dispatch with rate limiting and webhook secret auth - routers/approval.py: GET /approval/pending, POST /approval/respond - routers/registry.py: GET/POST /registry/agents, POST /registry/backend overrides - routers/sweep.py: POST /sweep trigger, GET /sweep/status - routers/health.py: GET /health, GET /health/detailed (DB/Odoo/Ollama checks) - requirements.txt: pinned deps (fastapi, uvicorn, asyncpg, anthropic, alembic) - Dockerfile: python:3.11-slim, single uvicorn worker - docker-compose.yml: agent-service + postgres:15, bound to 192.168.2.47:8001 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
agent_service/routers/approval.py
Normal file
89
agent_service/routers/approval.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix='/approval', tags=['approval'])
|
||||
|
||||
|
||||
class ApprovalRequest(BaseModel):
|
||||
directive_id: str
|
||||
approved: bool
|
||||
approver_id: str
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
class PendingApproval(BaseModel):
|
||||
directive_id: str
|
||||
agent: str
|
||||
action: str
|
||||
description: str
|
||||
created_at: str
|
||||
context: dict = {}
|
||||
|
||||
|
||||
@router.get('/pending', response_model=list[PendingApproval])
|
||||
async def list_pending():
|
||||
from ..app_state import get_db_pool
|
||||
pool = get_db_pool()
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='DB not ready')
|
||||
async with pool.acquire(timeout=10) as conn:
|
||||
rows = await conn.fetch(
|
||||
'SELECT directive_id, agent_name, action_type, description, created_at, context_data '
|
||||
'FROM ab_directive_log WHERE status = $1 ORDER BY created_at ASC',
|
||||
'pending_approval',
|
||||
)
|
||||
return [
|
||||
PendingApproval(
|
||||
directive_id=str(r['directive_id']),
|
||||
agent=r['agent_name'] or '',
|
||||
action=r['action_type'] or '',
|
||||
description=r['description'] or '',
|
||||
created_at=str(r['created_at']),
|
||||
context=r['context_data'] or {},
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post('/respond', status_code=status.HTTP_200_OK)
|
||||
async def respond_approval(req: ApprovalRequest):
|
||||
from ..app_state import get_db_pool, get_master_agent
|
||||
pool = get_db_pool()
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='DB not ready')
|
||||
|
||||
async with pool.acquire(timeout=10) as conn:
|
||||
row = await conn.fetchrow(
|
||||
'SELECT directive_id, status FROM ab_directive_log WHERE directive_id = $1',
|
||||
req.directive_id,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Directive not found')
|
||||
if row['status'] != 'pending_approval':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f'Directive is not pending approval (status={row["status"]})',
|
||||
)
|
||||
new_status = 'approved' if req.approved else 'rejected'
|
||||
await conn.execute(
|
||||
'UPDATE ab_directive_log SET status=$1, approver_id=$2, approval_note=$3, updated_at=NOW() '
|
||||
'WHERE directive_id=$4',
|
||||
new_status, req.approver_id, req.note, req.directive_id,
|
||||
)
|
||||
|
||||
logger.info('Directive %s %s by %s', req.directive_id, new_status, req.approver_id)
|
||||
|
||||
if req.approved:
|
||||
master = get_master_agent()
|
||||
if master:
|
||||
try:
|
||||
await master.resume_directive(req.directive_id)
|
||||
except Exception as exc:
|
||||
logger.error('resume_directive failed %s: %s', req.directive_id, exc)
|
||||
|
||||
return {'directive_id': req.directive_id, 'status': new_status}
|
||||
Reference in New Issue
Block a user