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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user