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:
Aissa
2026-08-01 17:59:32 +00:00
parent 5e43b6b246
commit 22a17ed048
8 changed files with 291 additions and 37 deletions
+18
View File
@@ -9,9 +9,15 @@ AGENT_NAME_MAP: dict[str, str] = {
"rss_feeds": "marketing",
"wholesale_scraper": "sourcing",
"halal_registry": "halal",
"halal_engine": "halal",
"halal_reco": "halal",
"branch_scraper": "sourcing",
"hermes": "herman",
"herman_delegate": "herman",
"ops": "sysops",
"sysop": "sysops",
"web_builder": "webbuilder",
"packaging_agent": "packaging",
}
@@ -20,3 +26,15 @@ def normalize_agent_key(name: str | None) -> str:
if not key:
return ""
return AGENT_NAME_MAP.get(key, key)
def aliases_for_key(canonical: str) -> list[str]:
"""All raw agent_name values that map to this soul key (including the key itself)."""
key = (canonical or "").strip().lower()
if not key:
return []
names = {key}
for raw, canon in AGENT_NAME_MAP.items():
if canon == key:
names.add(raw)
return sorted(names)
+138 -22
View File
@@ -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: