"""Agents API — souls & activity.""" from __future__ import annotations from typing import Any, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from app.db import fetch_all from app.services import agent_approvals, agent_souls router = APIRouter(prefix="/api/agents", tags=["agents-api"]) class SoulUpdate(BaseModel): display_name: Optional[str] = None role_title: Optional[str] = None soul_md: Optional[str] = None responsibilities: Optional[str] = None is_active: Optional[bool] = None class ActionRequestBody(BaseModel): agent_key: str title: str = Field(..., min_length=1, max_length=255) action_type: str = "query" query_payload: dict[str, Any] = Field(default_factory=dict) class RejectBody(BaseModel): reason: str = "" class ExecuteBody(BaseModel): result: dict[str, Any] = Field(default_factory=dict) @router.get("/souls") def api_list_souls() -> dict[str, Any]: return {"items": agent_souls.list_souls(), "count": len(agent_souls.list_souls())} @router.get("/souls/{agent_key}") def api_get_soul(agent_key: str) -> dict[str, Any]: soul = agent_souls.get_soul(agent_key) if not soul: raise HTTPException(404, "Agent not found") return {"soul": soul} @router.get("/souls/{agent_key}/events") def api_list_agent_events(agent_key: str, limit: int = 50) -> dict[str, Any]: soul = agent_souls.get_soul(agent_key) if not soul: raise HTTPException(404, "Agent not found") items = agent_souls.list_agent_events(agent_key, limit=min(limit, 100)) return {"agent_key": agent_key.lower(), "items": items, "count": len(items)} @router.put("/souls/{agent_key}") def api_update_soul(agent_key: str, body: SoulUpdate) -> dict[str, Any]: try: soul = agent_souls.update_soul(agent_key, **body.model_dump(exclude_none=True)) except ValueError as exc: raise HTTPException(404, str(exc)) from exc return {"soul": soul} @router.get("/mesh") def api_agents_mesh() -> dict[str, Any]: souls = agent_souls.list_souls() stats_rows = fetch_all( """ SELECT LOWER(agent_name) AS agent_key, MAX(created_at) AS last_event_at, COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '6 hours') AS events_6h, COUNT(*) FILTER ( WHERE created_at >= NOW() - INTERVAL '24 hours' AND status IN ('error', 'rejected') ) AS errors_24h FROM agent_events GROUP BY LOWER(agent_name) """ ) by_key = {str(r["agent_key"]): dict(r) for r in stats_rows} nodes: list[dict[str, Any]] = [] for soul in souls: key = str(soul.get("agent_key") or "").lower() row = by_key.get(key, {}) events_6h = int(row.get("events_6h") or 0) errors_24h = int(row.get("errors_24h") or 0) health = "offline" if events_6h > 0 and errors_24h == 0: health = "healthy" elif events_6h > 0: health = "warn" elif int(soul.get("event_count") or 0) > 0: health = "idle" node = dict(soul) node["health"] = health node["events_6h"] = events_6h node["errors_24h"] = errors_24h if row.get("last_event_at") is not None and hasattr(row["last_event_at"], "isoformat"): node["last_event_at"] = row["last_event_at"].isoformat() nodes.append(node) edge_rows = fetch_all( """ SELECT LOWER(agent_name) AS source_key, COUNT(*) AS weight FROM agent_events WHERE created_at >= NOW() - INTERVAL '24 hours' AND LOWER(agent_name) <> 'herman' GROUP BY LOWER(agent_name) ORDER BY weight DESC """ ) edges = [{"source": str(r["source_key"]), "target": "herman", "weight": int(r["weight"])} for r in edge_rows] return {"nodes": nodes, "edges": edges, "executives": [{"id": "ceo", "label": "CEO", "role": "Aissa"}, {"id": "cto", "label": "CTO", "role": "Platform"}]} @router.get("/approvals") def api_list_approvals(status: Optional[str] = None, limit: int = 50) -> dict[str, Any]: try: items = agent_approvals.list_requests(status=status, limit=limit) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc return {"items": items, "count": len(items)} @router.post("/requests") def api_create_action_request(body: ActionRequestBody) -> dict[str, Any]: try: req = agent_approvals.create_request( body.agent_key, body.title, action_type=body.action_type, query_payload=body.query_payload, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc return {"ok": True, "request": req, "message": "Wacht op goedkeuring voordat query uitgevoerd mag worden"} @router.post("/approvals/{request_id}/approve") def api_approve_request(request_id: int) -> dict[str, Any]: import os import httpx try: req = agent_approvals.approve_request(request_id, approved_by="ceo") action = req.get("action_type") or "" auto_result = None if action in ("config_backup", "maintenance_scan"): tools_url = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/") path = "/ops/backup/run" if action == "config_backup" else "/ops/maintenance/scan" payload = {"approval_request_id": request_id} if action == "config_backup" else None with httpx.Client(timeout=180.0) as client: resp = client.post(f"{tools_url}{path}", json=payload or {}) try: auto_result = resp.json() except Exception: auto_result = {"ok": False, "detail": resp.text} req = agent_approvals.mark_executed(request_id, auto_result or {}) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc return {"ok": True, "request": req, "executed": auto_result is not None, "result": auto_result} @router.post("/approvals/{request_id}/reject") def api_reject_request(request_id: int, body: RejectBody) -> dict[str, Any]: try: req = agent_approvals.reject_request(request_id, reason=body.reason) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc return {"ok": True, "request": req} @router.post("/approvals/{request_id}/execute") def api_execute_approved_request(request_id: int, body: ExecuteBody) -> dict[str, Any]: import os import httpx try: req = agent_approvals.require_approved(request_id) result_payload = body.result or {} action = req.get("action_type") or "" if action == "config_backup": tools_url = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/") with httpx.Client(timeout=180.0) as client: resp = client.post( f"{tools_url}/ops/backup/run", json={"approval_request_id": request_id}, ) try: result_payload = resp.json() except Exception: result_payload = {"ok": False, "detail": resp.text} if resp.status_code >= 400: raise HTTPException(status_code=502, detail=result_payload.get("detail") or resp.text) elif action == "maintenance_scan": tools_url = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/") with httpx.Client(timeout=120.0) as client: resp = client.post(f"{tools_url}/ops/maintenance/scan") result_payload = resp.json() if resp.status_code < 400 else {"ok": False, "detail": resp.text} req = agent_approvals.mark_executed(request_id, result_payload) except HTTPException: raise except PermissionError as exc: raise HTTPException(status_code=403, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc return {"ok": True, "request": req}