SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC

This commit is contained in:
sysops
2026-06-23 10:04:23 +00:00
parent 3bf15c4850
commit 26fe76afdd
165 changed files with 47427 additions and 1264 deletions
+355 -34
View File
@@ -1,13 +1,18 @@
"""Agents API — souls & activity."""
from __future__ import annotations
import asyncio
import json
from typing import Any, Optional
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app.db import fetch_all
from app.services import agent_approvals, agent_souls
from app.db import fetch_all, fetch_one
from app.services import agent_approvals, agent_integration, agent_souls
from app.services.agent_names import normalize_agent_key
from app.services.agent_terminal import execute_terminal_command
router = APIRouter(prefix="/api/agents", tags=["agents-api"])
@@ -35,6 +40,236 @@ class ExecuteBody(BaseModel):
result: dict[str, Any] = Field(default_factory=dict)
class HandoffBody(BaseModel):
from_agent: str
to_agent: str
handoff_type: str = "partner"
payload: dict[str, Any] = Field(default_factory=dict)
correlation_id: Optional[str] = None
class TerminalCommandBody(BaseModel):
command: str = Field(..., min_length=1, max_length=2000)
def _aggregate_agent_stats() -> dict[str, dict[str, Any]]:
from datetime import datetime, timedelta, timezone
rows = fetch_all(
"""
SELECT agent_name, title, status, created_at
FROM agent_events
WHERE created_at >= NOW() - INTERVAL '7 days'
ORDER BY created_at DESC
"""
)
now = datetime.now(timezone.utc)
by_key: dict[str, dict[str, Any]] = {}
for r in rows:
key = normalize_agent_key(str(r.get("agent_name") or ""))
if not key:
continue
bucket = by_key.setdefault(
key,
{"events_6h": 0, "errors_24h": 0, "last_event_at": None, "last_event_title": None},
)
created = r.get("created_at")
if bucket["last_event_at"] is None and created is not None:
bucket["last_event_at"] = created
bucket["last_event_title"] = r.get("title")
if created is not None:
if hasattr(created, "tzinfo") and created.tzinfo is None:
created = created.replace(tzinfo=timezone.utc)
if created >= now - timedelta(hours=6):
bucket["events_6h"] += 1
if created >= now - timedelta(hours=24) and str(r.get("status") or "") in ("error", "rejected"):
bucket["errors_24h"] += 1
return by_key
def _node_health(events_6h: int, errors_24h: int, event_count: int) -> str:
if events_6h > 0 and errors_24h == 0:
return "healthy"
if events_6h > 0:
return "warn"
if event_count > 0:
return "idle"
return "offline"
@router.get("/collaboration")
def api_collaboration() -> dict[str, Any]:
items = agent_integration.list_collaboration()
return {"items": items, "count": len(items)}
@router.post("/handoff")
def api_create_handoff(body: HandoffBody) -> dict[str, Any]:
try:
handoff = agent_integration.create_handoff(
body.from_agent,
body.to_agent,
handoff_type=body.handoff_type,
payload=body.payload,
correlation_id=body.correlation_id,
)
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
return {"ok": True, "handoff": handoff}
def _terminal_payload(ev: dict[str, Any]) -> dict[str, Any]:
if ev.get("created_at") and hasattr(ev["created_at"], "isoformat"):
ev["created_at"] = ev["created_at"].isoformat()
meta = ev.get("metadata")
if isinstance(meta, str):
try:
meta = json.loads(meta)
except Exception:
meta = {}
ev["metadata"] = meta or {}
line_type = "handoff" if "handoff" in str(ev.get("event_type") or "") else "action"
if str(ev.get("event_type") or "").startswith("terminal_"):
line_type = "command" if ev.get("event_type") == "terminal_in" else "output"
if ev["metadata"].get("target_agent"):
line_type = "handoff_out"
if ev["metadata"].get("source_agent"):
line_type = "handoff_in"
if str(ev.get("status") or "") in ("error", "rejected"):
line_type = "error"
return {
"type": line_type,
"id": ev.get("id"),
"agent": normalize_agent_key(ev.get("agent_name")),
"message": ev.get("title") or ev.get("event_type"),
"detail": (ev.get("body") or "")[:500],
"status": ev.get("status"),
"correlation_id": ev["metadata"].get("correlation_id"),
"at": ev.get("created_at"),
}
@router.get("/souls/{agent_key}/terminal/stream")
async def api_agent_terminal_stream(agent_key: str, correlation_id: Optional[str] = None):
key = normalize_agent_key(agent_key)
soul = agent_souls.get_soul(key)
if not soul:
raise HTTPException(404, "Agent not found")
async def event_gen():
last_id = 0
try:
hist = fetch_all(
"""
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
FROM agent_events
WHERE LOWER(agent_name) = %s
ORDER BY id DESC
LIMIT 30
""",
(key,),
)
for row in reversed(hist):
last_id = max(last_id, int(row["id"]))
payload = _terminal_payload(dict(row))
yield f"data: {json.dumps(payload, default=str)}\n\n"
except Exception:
pass
while True:
try:
if correlation_id:
rows = fetch_all(
"""
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
FROM agent_events
WHERE metadata->>'correlation_id' = %s
AND id > %s
ORDER BY id ASC
LIMIT 50
""",
(correlation_id, last_id),
)
else:
rows = fetch_all(
"""
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
FROM agent_events
WHERE LOWER(agent_name) = %s AND id > %s
ORDER BY id ASC
LIMIT 50
""",
(key, last_id),
)
for row in rows:
last_id = max(last_id, int(row["id"]))
payload = _terminal_payload(dict(row))
yield f"data: {json.dumps(payload, default=str)}\n\n"
except Exception as exc:
err = {"type": "error", "message": str(exc)}
yield f"data: {json.dumps(err)}\n\n"
await asyncio.sleep(0.4)
return StreamingResponse(
event_gen(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@router.get("/live/stream")
async def api_agents_live_stream():
"""Single realtime stream for all agent terminals."""
async def event_gen():
last_id = 0
try:
recent = fetch_all(
"""
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
FROM agent_events
WHERE created_at >= NOW() - INTERVAL '30 minutes'
ORDER BY id ASC
LIMIT 80
"""
)
for row in recent:
last_id = max(last_id, int(row["id"]))
payload = _terminal_payload(dict(row))
payload["replay"] = True
yield f"data: {json.dumps(payload, default=str)}\n\n"
except Exception:
pass
yield f"data: {json.dumps({'type': 'connected', 'last_id': last_id})}\n\n"
while True:
try:
rows = fetch_all(
"""
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
FROM agent_events
WHERE id > %s
ORDER BY id ASC
LIMIT 100
""",
(last_id,),
)
for row in rows:
last_id = max(last_id, int(row["id"]))
payload = _terminal_payload(dict(row))
yield f"data: {json.dumps(payload, default=str)}\n\n"
except Exception as exc:
err = {"type": "error", "message": str(exc)}
yield f"data: {json.dumps(err)}\n\n"
await asyncio.sleep(0.25)
return StreamingResponse(
event_gen(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@router.get("/souls")
def api_list_souls() -> dict[str, Any]:
return {"items": agent_souls.list_souls(), "count": len(agent_souls.list_souls())}
@@ -57,6 +292,16 @@ def api_list_agent_events(agent_key: str, limit: int = 50) -> dict[str, Any]:
return {"agent_key": agent_key.lower(), "items": items, "count": len(items)}
@router.post("/souls/{agent_key}/terminal/command")
async def api_terminal_command(agent_key: str, body: TerminalCommandBody) -> dict[str, Any]:
try:
return await execute_terminal_command(agent_key, body.command.strip())
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
@router.put("/souls/{agent_key}")
def api_update_soul(agent_key: str, body: SoulUpdate) -> dict[str, Any]:
try:
@@ -69,20 +314,7 @@ def api_update_soul(agent_key: str, body: SoulUpdate) -> dict[str, Any]:
@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}
by_key = _aggregate_agent_stats()
nodes: list[dict[str, Any]] = []
for soul in souls:
@@ -90,33 +322,112 @@ def api_agents_mesh() -> dict[str, Any]:
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"
health = _node_health(events_6h, errors_24h, int(soul.get("event_count") or 0))
is_active = health == "healthy" and events_6h > 0
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()
node["is_active"] = is_active
node["last_event_title"] = row.get("last_event_title")
lat = row.get("last_event_at")
if lat is not None and hasattr(lat, "isoformat"):
node["last_event_at"] = lat.isoformat()
nodes.append(node)
edge_rows = fetch_all(
report_edges: list[dict[str, Any]] = []
for node in nodes:
key = str(node.get("agent_key") or "").lower()
if key == "herman" or not node.get("is_active"):
continue
report_edges.append(
{
"source": key,
"target": "herman",
"type": "report",
"active": True,
"weight": int(node.get("events_6h") or 1),
}
)
delegate_rows = fetch_all(
"""
SELECT LOWER(agent_name) AS source_key, COUNT(*) AS weight
SELECT metadata, created_at
FROM agent_events
WHERE created_at >= NOW() - INTERVAL '24 hours'
AND LOWER(agent_name) <> 'herman'
GROUP BY LOWER(agent_name)
ORDER BY weight DESC
WHERE agent_name = 'herman'
AND created_at >= NOW() - INTERVAL '6 hours'
AND (
event_type IN ('openswarm_delegation', 'delegate', 'packaging_delivered', 'telegram_delegation')
OR metadata ? 'delegated'
)
ORDER BY created_at DESC
LIMIT 100
"""
)
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"}]}
delegate_edges: list[dict[str, Any]] = []
seen_delegate: set[tuple[str, str]] = set()
for r in delegate_rows:
meta = r.get("metadata") or {}
if isinstance(meta, str):
try:
meta = json.loads(meta)
except Exception:
meta = {}
delegated = meta.get("delegated") or meta.get("delegated_agents") or []
if isinstance(delegated, str):
delegated = [delegated]
channel = meta.get("channel") or "herman"
for agent in delegated:
tgt = normalize_agent_key(str(agent))
if not tgt or tgt == "herman":
continue
pair = ("herman", tgt)
if pair in seen_delegate:
continue
seen_delegate.add(pair)
delegate_edges.append(
{
"source": "herman",
"target": tgt,
"type": "delegate",
"active": True,
"channel": channel,
}
)
peer_static = [
{
"source": str(r["from_agent"]),
"target": str(r["to_agent"]),
"type": "static",
"handoff_type": r.get("handoff_type"),
}
for r in agent_integration.list_collaboration()
]
peer_live = agent_integration.peer_edges_live(hours=6)
peer_edges = peer_static + peer_live
herman_active = any(n.get("agent_key") == "herman" and n.get("is_active") for n in nodes)
herman_active = herman_active or bool(delegate_edges) or bool(report_edges)
executive_edges = []
if herman_active:
executive_edges = [
{"source": "herman", "target": "ceo", "type": "executive", "active": True},
{"source": "herman", "target": "cto", "type": "executive", "active": True},
]
return {
"nodes": nodes,
"report_edges": report_edges,
"delegate_edges": delegate_edges,
"peer_edges": peer_edges,
"executive_edges": executive_edges,
"edges": report_edges,
"executives": [
{"id": "ceo", "label": "CEO", "role": "Aissa"},
{"id": "cto", "label": "CTO", "role": "Platform"},
],
}
@router.get("/approvals")
@@ -141,7 +452,17 @@ def api_create_action_request(body: ActionRequestBody) -> dict[str, Any]:
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"}
auto = None
rid = (req or {}).get("id")
if rid:
try:
auto = agent_approvals.try_auto_approve_and_execute(int(rid))
except Exception:
auto = None
msg = "Wacht op goedkeuring voordat query uitgevoerd mag worden"
if auto:
msg = "Auto-goedgekeurd en uitgevoerd (SysOps policy)"
return {"ok": True, "request": req, "auto_executed": bool(auto), "message": msg}
@router.post("/approvals/{request_id}/approve")