"""Agent handoffs and collaboration matrix.""" from __future__ import annotations import json import uuid from typing import Any, Optional from app.db import execute, fetch_all, fetch_one from app.services.agent_names import normalize_agent_key def _serialize(row: dict[str, Any] | None) -> dict[str, Any] | None: if not row: return None out = dict(row) for k, v in list(out.items()): if hasattr(v, "isoformat"): out[k] = v.isoformat() elif k == "correlation_id" and v is not None: out[k] = str(v) return out def list_collaboration() -> list[dict[str, Any]]: rows = fetch_all( "SELECT from_agent, to_agent, handoff_type, description FROM agent_collaboration ORDER BY from_agent, to_agent" ) return [dict(r) for r in rows] def create_handoff( from_agent: str, to_agent: str, *, handoff_type: str = "partner", payload: dict[str, Any] | None = None, correlation_id: str | None = None, status: str = "completed", ) -> dict[str, Any]: src = normalize_agent_key(from_agent) dst = normalize_agent_key(to_agent) if not src or not dst: raise ValueError("from_agent and to_agent required") cid = correlation_id or str(uuid.uuid4()) try: uuid.UUID(str(cid)) except ValueError: cid = str(uuid.uuid4()) row = fetch_one( """ INSERT INTO agent_handoffs (correlation_id, from_agent, to_agent, handoff_type, payload, status, completed_at) VALUES (%s::uuid, %s, %s, %s, %s::jsonb, %s, CASE WHEN %s = 'completed' THEN NOW() ELSE NULL END) RETURNING * """, (cid, src, dst, handoff_type, json.dumps(payload or {}), status, status), ) handoff = _serialize(row) or {} meta = { "correlation_id": cid, "handoff_id": handoff.get("id"), "target_agent": dst, "source_agent": src, "handoff_type": handoff_type, } _log_handoff_events(src, dst, handoff_type, payload or {}, meta, cid) return handoff def _log_handoff_events( src: str, dst: str, handoff_type: str, payload: dict[str, Any], meta: dict[str, Any], cid: str, ) -> None: title_out = f"{src} → {dst}: {handoff_type}" title_in = f"Handoff van {src}: {handoff_type}" body = json.dumps(payload)[:2000] if payload else "" for agent, etype, title, extra in ( (src, "handoff_out", title_out, {"target_agent": dst}), (dst, "handoff_in", title_in, {"source_agent": src}), ): try: execute( """ INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb) """, ( agent, "agent_handoff", etype, title[:255], body, "completed", "agents", json.dumps({**meta, **extra}), ), ) except Exception: pass def recent_handoffs(hours: int = 6) -> list[dict[str, Any]]: rows = fetch_all( """ SELECT * FROM agent_handoffs WHERE created_at >= NOW() - make_interval(hours => %s) ORDER BY created_at DESC LIMIT 200 """, (max(1, min(hours, 168)),), ) return [_serialize(r) for r in rows if r] def peer_edges_live(hours: int = 6) -> list[dict[str, Any]]: rows = fetch_all( """ SELECT from_agent, to_agent, handoff_type, COUNT(*) AS weight, MAX(correlation_id::text) AS correlation_id FROM agent_handoffs WHERE created_at >= NOW() - make_interval(hours => %s) AND status = 'completed' GROUP BY from_agent, to_agent, handoff_type """, (max(1, min(hours, 168)),), ) return [ { "source": str(r["from_agent"]), "target": str(r["to_agent"]), "type": "live", "handoff_type": r.get("handoff_type"), "weight": int(r["weight"] or 1), "correlation_id": r.get("correlation_id"), } for r in rows ]