141 lines
4.2 KiB
Python
141 lines
4.2 KiB
Python
|
|
"""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
|
||
|
|
]
|