From 5f07a6b3aed85a8c8ba7e4d150d32229dd568c79 Mon Sep 17 00:00:00 2001 From: Apple Date: Tue, 2 Dec 2025 02:49:02 -0800 Subject: [PATCH] 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 --- services/city-service/repo_city.py | 267 ++++++++++++++++++++++++++- services/city-service/routes_city.py | 91 ++++++++- 2 files changed, 350 insertions(+), 8 deletions(-) diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py index 000c5841..693db710 100644 --- a/services/city-service/repo_city.py +++ b/services/city-service/repo_city.py @@ -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 + } diff --git a/services/city-service/routes_city.py b/services/city-service/routes_city.py index 02145fa2..c785eae4 100644 --- a/services/city-service/routes_city.py +++ b/services/city-service/routes_city.py @@ -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")