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]:
"""
Отримати URL endpoints для конкретної ноди.
Якщо в БД немає значень — підставляє дефолти для NODE1.
Якщо в БД немає значень — підставляє дефолти на основі node_id.
"""
pool = await get_pool()
@@ -3356,11 +3356,21 @@ async def get_node_endpoints(node_id: str) -> Dict[str, str]:
WHERE node_id = $1
""", node_id)
# Default values (NODE1 Docker-based)
defaults = {
"router_url": "http://dagi-router:9102",
"swapper_url": "http://swapper-service:8890"
}
# Determine defaults based on node_id
is_node2 = "node-2" in node_id.lower() or "macbook" in node_id.lower()
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:
return defaults
@@ -4051,3 +4061,248 @@ async def get_nodes_needing_healing() -> List[Dict[str, Any]]:
except Exception as e:
logger.error(f"Failed to get nodes needing healing: {e}")
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,
CreateAgentResponse,
DeleteAgentResponse,
CreateMicrodaoRoomRequest
CreateMicrodaoRoomRequest,
MicrodaoActivity,
CreateMicrodaoActivity,
MicrodaoStats,
MicrodaoDashboard
)
import repo_city
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.
Used by Node Cabinet to show loaded models and health.
Returns fallback data if metrics not found (instead of 404).
"""
try:
# Fetch from node_cache
metrics = await repo_city.get_node_metrics(node_id)
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)
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}")
# =============================================================================
# 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")