Files
foodlinkk-command-center/cockpit/app/services/agent_souls.py
T
Aissa 910ba465d7 Add War Room voice meeting, fix chat overlay, and ship 5 cyan UI themes.
Voice Room lets humans talk with agents via Whisper/TTS; chat typing no longer blocked by a full-screen overlay, attachments/avatars are larger, and Settings exposes five vivid mid-dark cyan/blue themes for the whole cockpit.
2026-08-16 15:49:29 +00:00

213 lines
7.7 KiB
Python

"""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 * 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
# is_active souls are never "offline" in the registry (War Room / mesh ready)
if not soul.get("is_active", True):
soul["activity"] = "offline"
else:
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")
)
out.append(soul)
return out
def get_soul(agent_key: str) -> Optional[dict[str, Any]]:
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(
f"""SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at, completed_at
FROM agent_events
WHERE LOWER(agent_name) IN ({placeholders})
ORDER BY created_at DESC
LIMIT %s""",
(*[n.lower() for n in names], limit),
)
out: list[dict[str, Any]] = []
for row in rows:
ev = dict(row)
for key in ("created_at", "completed_at"):
if ev.get(key) is not None and hasattr(ev[key], "isoformat"):
ev[key] = ev[key].isoformat()
out.append(ev)
return out
def update_soul(agent_key: str, **fields: Any) -> dict[str, Any]:
allowed = ("display_name", "role_title", "soul_md", "responsibilities", "permissions", "is_active")
sets, params = [], []
for k, v in fields.items():
if k in allowed and v is not None:
sets.append(f"{k} = %s")
params.append(v)
if not sets:
soul = get_soul(agent_key)
if not soul:
raise ValueError("Agent not found")
return soul
params.append(agent_key.lower())
execute(f"UPDATE agent_souls SET {', '.join(sets)}, updated_at = NOW() WHERE agent_key = %s", tuple(params))
return get_soul(agent_key) or {}
def list_permissions() -> list[dict[str, Any]]:
return [dict(r) for r in fetch_all("SELECT * FROM herman_permissions ORDER BY category, module_label")]
def update_permission(module_key: str, granted: bool) -> dict[str, Any]:
execute(
"""UPDATE herman_permissions SET granted = %s, granted_at = CASE WHEN %s THEN NOW() ELSE NULL END, updated_at = NOW()
WHERE module_key = %s""",
(granted, granted, module_key),
)
row = fetch_one("SELECT * FROM herman_permissions WHERE module_key = %s", (module_key,))
return dict(row or {})
def grant_all_permissions() -> int:
execute("UPDATE herman_permissions SET granted = TRUE, granted_at = NOW(), updated_at = NOW()")
row = fetch_one("SELECT COUNT(*) AS n FROM herman_permissions WHERE granted = TRUE")
return int((row or {}).get("n") or 0)