Files
microdao-daarion/gateway-bot/daarion_facade/registry_api.py

269 lines
9.1 KiB
Python

import json
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
import httpx
from fastapi import APIRouter
from redis.asyncio import Redis
router = APIRouter(prefix="/v1", tags=["daarion-facade"])
REGISTRY_CACHE_TTL = int(os.getenv("REGISTRY_CACHE_TTL", "30"))
MEMORY_SERVICE_URL = os.getenv("MEMORY_SERVICE_URL", "http://memory-service:8000")
CREWAI_SERVICE_URL = os.getenv("CREWAI_SERVICE_URL", "http://dagi-staging-crewai-service:9010")
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
METRICS_DASHBOARD_KEY = "daarion:metrics:dashboard"
_REGISTRY_CACHE: Dict[str, Any] = {"loaded_at": 0.0, "data": None}
_DISTRICT_CACHE: Dict[str, Any] = {"loaded_at": 0.0, "data": None}
_CREWAI_CACHE: Dict[str, Any] = {"loaded_at": 0.0, "data": {}}
_REDIS: Optional[Redis] = None
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _registry_paths() -> List[Path]:
return [
Path("/app/gateway-bot/agent_registry.json"),
Path("/opt/microdao-daarion/config/agent_registry.json"),
Path(__file__).resolve().parents[1] / "agent_registry.json",
]
def _district_paths() -> List[Path]:
return [
Path("/app/gateway-bot/district_registry.json"),
Path(__file__).resolve().parents[1] / "district_registry.json",
]
def _load_registry() -> Dict[str, Any]:
now = time.time()
if _REGISTRY_CACHE.get("data") and (now - _REGISTRY_CACHE.get("loaded_at", 0.0) < REGISTRY_CACHE_TTL):
return _REGISTRY_CACHE["data"]
for path in _registry_paths():
if path.exists():
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
_REGISTRY_CACHE.update({"loaded_at": now, "data": data})
return data
data = {"agents": {}}
_REGISTRY_CACHE.update({"loaded_at": now, "data": data})
return data
def _load_district_registry() -> Dict[str, Any]:
now = time.time()
if _DISTRICT_CACHE.get("data") and (now - _DISTRICT_CACHE.get("loaded_at", 0.0) < REGISTRY_CACHE_TTL):
return _DISTRICT_CACHE["data"]
for path in _district_paths():
if path.exists():
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
_DISTRICT_CACHE.update({"loaded_at": now, "data": data})
return data
data = {"districts": []}
_DISTRICT_CACHE.update({"loaded_at": now, "data": data})
return data
async def _redis_client() -> Redis:
global _REDIS
if _REDIS is None:
_REDIS = Redis.from_url(REDIS_URL, decode_responses=True)
return _REDIS
async def _load_cached_dashboard() -> Optional[Dict[str, Any]]:
try:
r = await _redis_client()
raw = await r.get(METRICS_DASHBOARD_KEY)
if not raw:
return None
return json.loads(raw)
except Exception:
return None
async def _load_crewai_roles() -> Dict[str, int]:
now = time.time()
if now - _CREWAI_CACHE.get("loaded_at", 0.0) < REGISTRY_CACHE_TTL:
return _CREWAI_CACHE.get("data", {})
out: Dict[str, int] = {}
try:
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.get(f"{CREWAI_SERVICE_URL}/crew/agents")
if resp.status_code == 200:
payload = resp.json()
for aid, info in payload.items():
default_roles = info.get("default_roles")
out[str(aid)] = int(default_roles) if isinstance(default_roles, int) else 0
except Exception:
out = {}
_CREWAI_CACHE.update({"loaded_at": now, "data": out})
return out
@router.get("/registry/agents")
async def get_agents() -> Dict[str, Any]:
reg = _load_registry()
agents = reg.get("agents", {}) if isinstance(reg, dict) else {}
role_counts = await _load_crewai_roles()
items: List[Dict[str, Any]] = []
for agent_id, cfg in agents.items():
if not isinstance(cfg, dict):
continue
domains = cfg.get("domains") or []
district_id = cfg.get("district_id") or "city-core"
items.append(
{
"agent_id": agent_id,
"title": cfg.get("display_name") or agent_id,
"role": cfg.get("canonical_role") or "",
"domain_primary": domains[0] if domains else "general",
"domain_aliases": domains[1:] if len(domains) > 1 else [],
"visibility": cfg.get("visibility", "public"),
"status": cfg.get("status", "active"),
"team": {"subagents_total": role_counts.get(agent_id, 0)},
"district_id": district_id,
"avatar_url": cfg.get("avatar_url"),
"health_url": cfg.get("health_url"),
}
)
return {"items": items, "total": len(items)}
@router.get("/registry/districts")
async def get_districts() -> Dict[str, Any]:
agents_payload = await get_agents()
agents = agents_payload.get("items", [])
by_district: Dict[str, List[Dict[str, Any]]] = {}
for a in agents:
by_district.setdefault(a.get("district_id", "city-core"), []).append(a)
catalog = _load_district_registry().get("districts", [])
catalog_by_id: Dict[str, Dict[str, Any]] = {
str(d.get("district_id")): d for d in catalog if isinstance(d, dict) and d.get("district_id")
}
district_ids = sorted(set(catalog_by_id.keys()) | set(by_district.keys()))
items: List[Dict[str, Any]] = []
for district_id in district_ids:
members = by_district.get(district_id, [])
base = catalog_by_id.get(district_id, {})
domain = base.get("domain") or ("daarion.city" if district_id == "city-core" else f"{district_id}.daarion.city")
lead_agent_id = base.get("lead_agent_id")
if not lead_agent_id:
if district_id == "city-core" and any(m.get("agent_id") == "daarwizz" for m in members):
lead_agent_id = "daarwizz"
elif members:
lead_agent_id = members[0].get("agent_id")
else:
lead_agent_id = None
items.append(
{
"district_id": district_id,
"title": base.get("title") or district_id.replace("-", " ").title(),
"domain": domain,
"status": base.get("status", "active"),
"logo_url": base.get("logo_url"),
"health_url": base.get("health_url"),
"well_known": {
"manifest": f"https://{domain}/.well-known/daarion-district.json",
"health": f"https://{domain}/.well-known/daarion-health.json",
"capabilities": f"https://{domain}/.well-known/daarion-capabilities.json",
},
"lead_agent_id": lead_agent_id,
"agents_total": len(members),
}
)
return {"items": items, "total": len(items)}
@router.get("/metrics")
async def get_metrics() -> Dict[str, Any]:
agents_payload = await get_agents()
districts_payload = await get_districts()
agents = agents_payload.get("items", [])
memory_vectors = 0
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{MEMORY_SERVICE_URL}/health")
if resp.status_code == 200:
data = resp.json()
memory_vectors = int(
data.get("vector_store", {})
.get("memories", {})
.get("vectors_count", 0)
)
except Exception:
memory_vectors = 0
return {
"nodes": 1,
"districts": districts_payload.get("total", 0),
"agents": len(agents),
"subagents": sum(int((a.get("team") or {}).get("subagents_total", 0)) for a in agents),
"memory_vectors": memory_vectors,
}
@router.get("/metrics/dashboard")
async def get_metrics_dashboard() -> Dict[str, Any]:
cached = await _load_cached_dashboard()
if cached:
return cached
metrics = await get_metrics()
districts_payload = await get_districts()
districts = districts_payload.get("items", [])
by_district = []
for d in districts:
by_district.append(
{
"district_id": d.get("district_id"),
"title": d.get("title"),
"domain": d.get("domain"),
"status": d.get("status"),
"ok": None,
"agents_total": d.get("agents_total", 0),
"agents_online": None,
"latency_ms": None,
"last_check_ts": None,
}
)
return {
"global": {
"nodes": metrics.get("nodes", 1),
"districts": metrics.get("districts", 0),
"agents": metrics.get("agents", 0),
"subagents": metrics.get("subagents", 0),
"memory_vectors": metrics.get("memory_vectors", 0),
"districts_online": 0,
"agents_online": 0,
},
"by_district": by_district,
"updated_at": _now_iso(),
"source": "fallback_registry",
}