227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
"""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}
|