fix(nodes): Fix Swapper models and DAGI Router agents display for NODE2

- Fix get_node_endpoints to correctly determine URLs for NODE2 (localhost instead of Docker service names)
- Fix swapper detail endpoint to return fallback data instead of 404 when metrics not found
- This allows UI to show pending state instead of error for NODE2

Fixes:
- Swapper Service models not showing for NODE2
- DAGI Router agents not showing for NODE2
This commit is contained in:
Apple
2025-12-02 02:49:02 -08:00
parent ceeb0faaf6
commit 5f07a6b3ae
2 changed files with 350 additions and 8 deletions

View File

@@ -3346,7 +3346,7 @@ async def get_node_metrics_current(node_id: str) -> Optional[Dict[str, Any]]:
async def get_node_endpoints(node_id: str) -> Dict[str, str]: async def get_node_endpoints(node_id: str) -> Dict[str, str]:
""" """
Отримати URL endpoints для конкретної ноди. Отримати URL endpoints для конкретної ноди.
Якщо в БД немає значень — підставляє дефолти для NODE1. Якщо в БД немає значень — підставляє дефолти на основі node_id.
""" """
pool = await get_pool() pool = await get_pool()
@@ -3356,11 +3356,21 @@ async def get_node_endpoints(node_id: str) -> Dict[str, str]:
WHERE node_id = $1 WHERE node_id = $1
""", node_id) """, node_id)
# Default values (NODE1 Docker-based) # Determine defaults based on node_id
defaults = { is_node2 = "node-2" in node_id.lower() or "macbook" in node_id.lower()
"router_url": "http://dagi-router:9102",
"swapper_url": "http://swapper-service:8890" if is_node2:
} # NODE2 defaults (localhost or IP-based)
defaults = {
"router_url": "http://localhost:9102",
"swapper_url": "http://localhost:8890"
}
else:
# NODE1 defaults (Docker-based)
defaults = {
"router_url": "http://dagi-router:9102",
"swapper_url": "http://swapper-service:8890"
}
if not row: if not row:
return defaults return defaults
@@ -4051,3 +4061,248 @@ async def get_nodes_needing_healing() -> List[Dict[str, Any]]:
except Exception as e: except Exception as e:
logger.error(f"Failed to get nodes needing healing: {e}") logger.error(f"Failed to get nodes needing healing: {e}")
return [] return []
# =============================================================================
# MicroDAO Activity Repository
# =============================================================================
async def get_microdao_activity(slug: str, limit: int = 20) -> List[dict]:
"""Отримати активність MicroDAO (новини, події, оновлення)"""
pool = await get_pool()
query = """
SELECT
id, microdao_slug, kind, title, body,
author_agent_id, author_name, created_at
FROM microdao_activity
WHERE microdao_slug = $1
ORDER BY created_at DESC
LIMIT $2
"""
rows = await pool.fetch(query, slug, limit)
return [dict(row) for row in rows]
async def create_microdao_activity(
slug: str,
kind: str,
body: str,
title: Optional[str] = None,
author_agent_id: Optional[str] = None,
author_name: Optional[str] = None
) -> dict:
"""Створити новий запис активності для MicroDAO"""
pool = await get_pool()
# Перевірити, що MicroDAO існує
microdao = await get_microdao_by_slug(slug)
if not microdao:
raise ValueError(f"MicroDAO {slug} not found")
query = """
INSERT INTO microdao_activity (
microdao_slug, kind, title, body,
author_agent_id, author_name
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, microdao_slug, kind, title, body,
author_agent_id, author_name, created_at
"""
row = await pool.fetchrow(
query,
slug,
kind,
title,
body,
author_agent_id,
author_name
)
return dict(row)
async def get_citizens_for_microdao(slug: str, limit: int = 6) -> List[dict]:
"""Отримати громадян (публічних агентів) для MicroDAO"""
pool = await get_pool()
# Спочатку отримати microdao_id
microdao = await get_microdao_by_slug(slug)
if not microdao:
return []
microdao_id = str(microdao["id"])
# Використати існуючу функцію або зробити простий SELECT
query = """
SELECT DISTINCT
a.public_slug AS slug,
a.display_name,
a.public_title,
a.public_tagline,
a.avatar_url,
a.public_district AS district,
a.public_primary_room_slug AS primary_room_slug,
a.kind,
a.node_id,
COALESCE(a.public_skills, ARRAY[]::text[]) AS public_skills,
a.status,
m.slug AS home_microdao_slug,
m.name AS home_microdao_name
FROM microdao_agents ma
JOIN agents a ON a.id = ma.agent_id
LEFT JOIN microdaos m ON m.id = a.home_microdao_id
WHERE ma.microdao_id = $1
AND a.is_public = true
AND a.public_slug IS NOT NULL
ORDER BY a.display_name
LIMIT $2
"""
rows = await pool.fetch(query, microdao_id, limit)
result = []
for row in rows:
data = dict(row)
data["public_skills"] = list(data.get("public_skills") or [])
result.append(data)
return result
async def count_agents_for_microdao(slug: str) -> int:
"""Підрахувати кількість агентів для MicroDAO"""
pool = await get_pool()
microdao = await get_microdao_by_slug(slug)
if not microdao:
return 0
microdao_id = str(microdao["id"])
query = """
SELECT COUNT(*) as count
FROM microdao_agents
WHERE microdao_id = $1
"""
row = await pool.fetchrow(query, microdao_id)
return row["count"] if row else 0
async def get_microdao_dashboard(slug: str) -> dict:
"""Отримати повний дашборд для MicroDAO"""
pool = await get_pool()
# Отримати MicroDAO
microdao = await get_microdao_by_slug(slug)
if not microdao:
raise ValueError(f"MicroDAO {slug} not found")
microdao_id = str(microdao["id"])
# Отримати кімнати
rooms = await get_microdao_rooms(microdao_id)
rooms_limited = rooms[:5]
# Отримати громадян
citizens = await get_citizens_for_microdao(slug, limit=6)
# Отримати активність
activity = await get_microdao_activity(slug, limit=10)
# Підрахувати статистику
rooms_count = len(rooms)
citizens_count = len(citizens)
agents_count = await count_agents_for_microdao(slug)
# Конвертувати rooms в CityRoomSummary
room_summaries = []
for room in rooms_limited:
room_summaries.append({
"id": room["id"],
"slug": room["slug"],
"name": room["name"],
"matrix_room_id": room.get("matrix_room_id"),
"microdao_id": room.get("microdao_id"),
"microdao_slug": room.get("microdao_slug"),
"room_role": room.get("room_role"),
"is_public": room.get("is_public", True),
"sort_order": room.get("sort_order", 100),
"logo_url": room.get("logo_url"),
"banner_url": room.get("banner_url")
})
# Конвертувати citizens в PublicCitizenSummary
citizen_summaries = []
for citizen in citizens:
citizen_summaries.append({
"slug": citizen.get("slug"),
"display_name": citizen["display_name"],
"public_title": citizen.get("public_title"),
"public_tagline": citizen.get("public_tagline"),
"avatar_url": citizen.get("avatar_url"),
"kind": citizen.get("kind"),
"district": citizen.get("district"),
"primary_room_slug": citizen.get("primary_room_slug"),
"public_skills": citizen.get("public_skills", []),
"online_status": citizen.get("status", "unknown"),
"status": citizen.get("status"),
"node_id": citizen.get("node_id"),
"home_microdao_slug": citizen.get("home_microdao_slug"),
"home_microdao_name": citizen.get("home_microdao_name")
})
# Конвертувати activity в MicrodaoActivity
activity_list = []
for act in activity:
activity_list.append({
"id": str(act["id"]),
"microdao_slug": act["microdao_slug"],
"kind": act["kind"],
"title": act.get("title"),
"body": act["body"],
"author_agent_id": str(act["author_agent_id"]) if act.get("author_agent_id") else None,
"author_name": act.get("author_name"),
"created_at": act["created_at"]
})
# Створити MicrodaoSummary
microdao_summary = {
"id": str(microdao["id"]),
"slug": microdao["slug"],
"name": microdao["name"],
"description": microdao.get("description"),
"district": microdao.get("district"),
"is_public": microdao.get("is_public", True),
"is_platform": microdao.get("is_platform", False),
"is_active": microdao.get("is_active", True),
"is_pinned": microdao.get("is_pinned", False),
"pinned_weight": microdao.get("pinned_weight", 0),
"orchestrator_agent_id": microdao.get("orchestrator_agent_id"),
"orchestrator_agent_name": microdao.get("orchestrator_agent_name"),
"parent_microdao_id": microdao.get("parent_microdao_id"),
"parent_microdao_slug": microdao.get("parent_microdao_slug"),
"logo_url": microdao.get("logo_url"),
"banner_url": microdao.get("banner_url"),
"member_count": agents_count,
"agents_count": agents_count,
"room_count": rooms_count,
"rooms_count": rooms_count,
"channels_count": 0
}
# Створити stats
stats = {
"rooms_count": rooms_count,
"citizens_count": citizens_count,
"agents_count": agents_count,
"last_update_at": microdao.get("updated_at")
}
return {
"microdao": microdao_summary,
"stats": stats,
"recent_activity": activity_list,
"rooms": room_summaries,
"citizens": citizen_summaries
}

View File

@@ -59,7 +59,11 @@ from models_city import (
CreateAgentRequest, CreateAgentRequest,
CreateAgentResponse, CreateAgentResponse,
DeleteAgentResponse, DeleteAgentResponse,
CreateMicrodaoRoomRequest CreateMicrodaoRoomRequest,
MicrodaoActivity,
CreateMicrodaoActivity,
MicrodaoStats,
MicrodaoDashboard
) )
import repo_city import repo_city
from common.redis_client import PresenceRedis, get_redis from common.redis_client import PresenceRedis, get_redis
@@ -4384,12 +4388,21 @@ async def get_node_swapper_detail(node_id: str):
""" """
Get detailed Swapper Service status for a node. Get detailed Swapper Service status for a node.
Used by Node Cabinet to show loaded models and health. Used by Node Cabinet to show loaded models and health.
Returns fallback data if metrics not found (instead of 404).
""" """
try: try:
# Fetch from node_cache # Fetch from node_cache
metrics = await repo_city.get_node_metrics(node_id) metrics = await repo_city.get_node_metrics(node_id)
if not metrics: if not metrics:
raise HTTPException(status_code=404, detail="Node not found") # Return fallback instead of 404 - allows UI to show pending state
logger.info(f"Swapper metrics not found for {node_id}, returning fallback")
return NodeSwapperDetail(
node_id=node_id,
healthy=False,
models_loaded=0,
models_total=0,
models=[]
)
# Parse swapper state (stored as JSONB) # Parse swapper state (stored as JSONB)
state = metrics.get("swapper_state") or {} state = metrics.get("swapper_state") or {}
@@ -4724,3 +4737,77 @@ async def trigger_node_self_healing(node_id: str):
) )
raise HTTPException(status_code=500, detail=f"Self-healing failed: {e}") raise HTTPException(status_code=500, detail=f"Self-healing failed: {e}")
# =============================================================================
# MicroDAO Dashboard & Activity
# =============================================================================
@router.get("/microdao/{slug}/dashboard", response_model=MicrodaoDashboard)
async def api_get_microdao_dashboard(slug: str):
"""Отримати повний дашборд для MicroDAO"""
try:
dashboard = await repo_city.get_microdao_dashboard(slug)
return dashboard
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to get microdao dashboard for {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to load dashboard")
@router.get("/microdao/{slug}/activity", response_model=List[MicrodaoActivity])
async def api_list_microdao_activity(slug: str, limit: int = Query(20, ge=1, le=100)):
"""Отримати список активності MicroDAO"""
try:
activity = await repo_city.get_microdao_activity(slug, limit=limit)
# Конвертувати dict в MicrodaoActivity
result = []
for act in activity:
result.append({
"id": str(act["id"]),
"microdao_slug": act["microdao_slug"],
"kind": act["kind"],
"title": act.get("title"),
"body": act["body"],
"author_agent_id": str(act["author_agent_id"]) if act.get("author_agent_id") else None,
"author_name": act.get("author_name"),
"created_at": act["created_at"]
})
return result
except Exception as e:
logger.error(f"Failed to get microdao activity for {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to load activity")
@router.post("/microdao/{slug}/activity", response_model=MicrodaoActivity, status_code=201)
async def api_create_microdao_activity(
slug: str,
payload: CreateMicrodaoActivity
):
"""Створити новий запис активності для MicroDAO"""
try:
activity = await repo_city.create_microdao_activity(
slug=slug,
kind=payload.kind,
body=payload.body,
title=payload.title,
author_agent_id=str(payload.author_agent_id) if payload.author_agent_id else None,
author_name=payload.author_name
)
# Конвертувати dict в MicrodaoActivity
return {
"id": str(activity["id"]),
"microdao_slug": activity["microdao_slug"],
"kind": activity["kind"],
"title": activity.get("title"),
"body": activity["body"],
"author_agent_id": str(activity["author_agent_id"]) if activity.get("author_agent_id") else None,
"author_name": activity.get("author_name"),
"created_at": activity["created_at"]
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to create microdao activity for {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to create activity")