Activate agent mesh with honest live status and working animations.
Souls now aggregate aliased events (e.g. halal_engine), KPIs reflect 6h activity, active agents animate, and morning scheduler/scripts target cockpit :8610.
This commit is contained in:
@@ -1,49 +1,165 @@
|
||||
"""Agent soul profiles and Herman permissions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.db import execute, fetch_all, fetch_one
|
||||
from app.services.agent_names import aliases_for_key, normalize_agent_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"
|
||||
|
||||
|
||||
def _aggregate_event_stats() -> dict[str, dict[str, Any]]:
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT agent_name, title, status, created_at
|
||||
FROM agent_events
|
||||
WHERE created_at >= NOW() - INTERVAL '30 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,
|
||||
{
|
||||
"event_count": 0,
|
||||
"events_6h": 0,
|
||||
"events_24h": 0,
|
||||
"errors_24h": 0,
|
||||
"last_event_at": None,
|
||||
"last_event_title": None,
|
||||
"current_status": None,
|
||||
"current_event_type": None,
|
||||
},
|
||||
)
|
||||
bucket["event_count"] += 1
|
||||
created = r.get("created_at")
|
||||
if bucket["last_event_at"] is None:
|
||||
bucket["last_event_at"] = created
|
||||
bucket["last_event_title"] = r.get("title")
|
||||
bucket["current_status"] = r.get("status")
|
||||
if created is not None:
|
||||
if hasattr(created, "tzinfo") and created.tzinfo is None:
|
||||
created = created.replace(tzinfo=timezone.utc)
|
||||
age_ok = True
|
||||
try:
|
||||
if created >= now - timedelta(hours=6):
|
||||
bucket["events_6h"] += 1
|
||||
if created >= now - timedelta(hours=24):
|
||||
bucket["events_24h"] += 1
|
||||
if str(r.get("status") or "") in ("error", "rejected"):
|
||||
bucket["errors_24h"] += 1
|
||||
except Exception:
|
||||
age_ok = False
|
||||
_ = age_ok
|
||||
# Also count older totals beyond 30d window lightly
|
||||
try:
|
||||
totals = fetch_all(
|
||||
"""
|
||||
SELECT agent_name, COUNT(*)::int AS c
|
||||
FROM agent_events
|
||||
GROUP BY agent_name
|
||||
"""
|
||||
)
|
||||
total_by: dict[str, int] = {}
|
||||
for r in totals:
|
||||
k = normalize_agent_key(str(r.get("agent_name") or ""))
|
||||
if not k:
|
||||
continue
|
||||
total_by[k] = total_by.get(k, 0) + int(r.get("c") or 0)
|
||||
for k, c in total_by.items():
|
||||
bucket = by_key.setdefault(
|
||||
k,
|
||||
{
|
||||
"event_count": 0,
|
||||
"events_6h": 0,
|
||||
"events_24h": 0,
|
||||
"errors_24h": 0,
|
||||
"last_event_at": None,
|
||||
"last_event_title": None,
|
||||
"current_status": None,
|
||||
"current_event_type": None,
|
||||
},
|
||||
)
|
||||
bucket["event_count"] = c
|
||||
except Exception:
|
||||
pass
|
||||
return by_key
|
||||
|
||||
|
||||
def list_souls() -> list[dict[str, Any]]:
|
||||
rows = fetch_all(
|
||||
"""SELECT s.*,
|
||||
(SELECT COUNT(*) FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key) AS event_count,
|
||||
(SELECT MAX(created_at) FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key) AS last_event_at,
|
||||
(SELECT title FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key
|
||||
ORDER BY created_at DESC LIMIT 1) AS current_task,
|
||||
(SELECT status FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key
|
||||
ORDER BY created_at DESC LIMIT 1) AS current_status,
|
||||
(SELECT event_type FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key
|
||||
ORDER BY created_at DESC LIMIT 1) AS current_event_type
|
||||
FROM agent_souls s ORDER BY s.display_name"""
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
rows = fetch_all("SELECT * FROM agent_souls ORDER BY display_name")
|
||||
stats = _aggregate_event_stats()
|
||||
out: list[dict[str, Any]] = []
|
||||
for r in rows:
|
||||
soul = dict(r)
|
||||
key = str(soul.get("agent_key") or "").lower()
|
||||
st = stats.get(key, {})
|
||||
events_6h = int(st.get("events_6h") or 0)
|
||||
errors_24h = int(st.get("errors_24h") or 0)
|
||||
event_count = int(st.get("event_count") or 0)
|
||||
health = _node_health(events_6h, errors_24h, event_count)
|
||||
lat = st.get("last_event_at")
|
||||
if lat is not None and hasattr(lat, "isoformat"):
|
||||
lat = lat.isoformat()
|
||||
soul["event_count"] = event_count
|
||||
soul["events_6h"] = events_6h
|
||||
soul["events_24h"] = int(st.get("events_24h") or 0)
|
||||
soul["errors_24h"] = errors_24h
|
||||
soul["health"] = health
|
||||
soul["last_event_at"] = lat
|
||||
soul["current_task"] = st.get("last_event_title") or soul.get("current_task")
|
||||
soul["current_status"] = st.get("current_status")
|
||||
# Working = recent activity (not just DB is_active flag)
|
||||
soul["working"] = events_6h > 0
|
||||
soul["activity"] = (
|
||||
"busy" if (str(soul.get("current_status") or "").lower() in ("needs_approval", "pending") or events_6h >= 5)
|
||||
else ("active" if events_6h > 0 else ("idle" if event_count > 0 else "offline"))
|
||||
)
|
||||
out.append(soul)
|
||||
return out
|
||||
|
||||
|
||||
def get_soul(agent_key: str) -> Optional[dict[str, Any]]:
|
||||
row = fetch_one(
|
||||
"""SELECT s.*,
|
||||
(SELECT COUNT(*) FROM agent_events e WHERE LOWER(e.agent_name) = s.agent_key) AS event_count
|
||||
FROM agent_souls s WHERE agent_key = %s""",
|
||||
(agent_key.lower(),),
|
||||
)
|
||||
row = fetch_one("SELECT * FROM agent_souls WHERE agent_key = %s", (agent_key.lower(),))
|
||||
if not row:
|
||||
return None
|
||||
out = dict(row)
|
||||
key = agent_key.lower()
|
||||
stats = _aggregate_event_stats().get(key, {})
|
||||
out["event_count"] = int(stats.get("event_count") or 0)
|
||||
out["events_6h"] = int(stats.get("events_6h") or 0)
|
||||
out["health"] = _node_health(out["events_6h"], int(stats.get("errors_24h") or 0), out["event_count"])
|
||||
out["recent_events"] = list_agent_events(agent_key, limit=15)
|
||||
return out
|
||||
|
||||
|
||||
def list_agent_events(agent_key: str, limit: int = 50) -> list[dict[str, Any]]:
|
||||
names = aliases_for_key(agent_key)
|
||||
if not names:
|
||||
return []
|
||||
placeholders = ",".join(["%s"] * len(names))
|
||||
rows = fetch_all(
|
||||
"""SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at, completed_at
|
||||
f"""SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at, completed_at
|
||||
FROM agent_events
|
||||
WHERE LOWER(agent_name) = %s
|
||||
WHERE LOWER(agent_name) IN ({placeholders})
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s""",
|
||||
(agent_key.lower(), limit),
|
||||
(*[n.lower() for n in names], limit),
|
||||
)
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
|
||||
Reference in New Issue
Block a user