diff --git a/apps/web/src/app/agents/[agentId]/page.tsx b/apps/web/src/app/agents/[agentId]/page.tsx index d2defb00..f711471b 100644 --- a/apps/web/src/app/agents/[agentId]/page.tsx +++ b/apps/web/src/app/agents/[agentId]/page.tsx @@ -18,7 +18,8 @@ import { import { api, Agent, AgentInvokeResponse } from '@/lib/api'; import { VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents'; import { updateAgentVisibility, AgentVisibilityUpdate } from '@/lib/api/agents'; -import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2 } from 'lucide-react'; +import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2, MessageCircle } from 'lucide-react'; +import { CityChatWidget } from '@/components/city/CityChatWidget'; // Tab types type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat'; @@ -461,70 +462,106 @@ export default function AgentConsolePage() { {/* Chat Tab */} {activeTab === 'chat' && ( -
- {/* Messages */} -
- {messages.length === 0 && ( -
- -

Start a conversation with {profile?.display_name || agentId}

-
- )} - {messages.map(msg => ( -
-
-

{msg.content}

- {msg.meta && ( -
- {msg.meta.latency_ms && {msg.meta.latency_ms}ms} - {msg.meta.tokens_out && {msg.meta.tokens_out} tokens} -
- )} +
+ {/* Direct Chat with Agent via DAGI Router */} +
+
+

+ + Прямий чат з агентом +

+

Спілкування через DAGI Router

+
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+ +

Start a conversation with {profile?.display_name || agentId}

-
- ))} - {invoking && ( -
-
-
- - Thinking... + )} + {messages.map(msg => ( +
+
+

{msg.content}

+ {msg.meta && ( +
+ {msg.meta.latency_ms && {msg.meta.latency_ms}ms} + {msg.meta.tokens_out && {msg.meta.tokens_out} tokens} +
+ )}
+ ))} + {invoking && ( +
+
+
+ + Thinking... +
+
+
+ )} +
+
+ + {/* Input */} +
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()} + placeholder="Type a message..." + className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50" + disabled={invoking} + /> +
- )} -
+
- {/* Input */} -
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()} - placeholder="Type a message..." - className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50" - disabled={invoking} - /> - -
+ {/* Matrix City Room Chat */} +
+

+ + Публічна кімната агента +

+ + {dashboard?.primary_city_room ? ( +
+

+ Matrix-чат у кімнаті: {dashboard.primary_city_room.name} +

+ +
+ ) : ( +
+ +

Для цього агента ще не налаштована публічна кімната.

+

+ Прив'яжіть агента до MicroDAO або створіть кімнату в City Service. +

+
+ )}
)} diff --git a/apps/web/src/app/microdao/[slug]/page.tsx b/apps/web/src/app/microdao/[slug]/page.tsx index da79f19a..285b938f 100644 --- a/apps/web/src/app/microdao/[slug]/page.tsx +++ b/apps/web/src/app/microdao/[slug]/page.tsx @@ -5,7 +5,8 @@ import Link from "next/link"; import { useMicrodaoDetail } from "@/hooks/useMicrodao"; import { DISTRICT_COLORS } from "@/lib/microdao"; import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard"; -import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot } from "lucide-react"; +import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot, MessageCircle } from "lucide-react"; +import { CityChatWidget } from "@/components/city/CityChatWidget"; export default function MicrodaoDetailPage() { const params = useParams(); @@ -372,6 +373,36 @@ export default function MicrodaoDetailPage() {
+ {/* Public Chat Room */} +
+

+ + Публічний чат MicroDAO +

+ + {microdao.primary_city_room ? ( +
+

+ Matrix-чат у кімнаті: {microdao.primary_city_room.name} +

+ {orchestrator && ( +

+ Оркестратор: {orchestrator.display_name} +

+ )} + +
+ ) : ( +
+ +

Для цього MicroDAO ще не налаштована публічна кімната.

+

+ Налаштуйте primary room у City Service, щоб увімкнути чат. +

+
+ )} +
+ {/* Visibility Settings (only for orchestrator) */} {orchestrator && (
+ {/* Node Guardian & Steward Agents */} + @@ -245,6 +251,14 @@ export default function NodeCabinetPage() { )}
+ {/* Node Guardian & Steward Agents */} +
+ +
+ {/* Notice for non-NODE1 */}

diff --git a/apps/web/src/components/nodes/NodeAgentsPanel.tsx b/apps/web/src/components/nodes/NodeAgentsPanel.tsx new file mode 100644 index 00000000..3a3eb5db --- /dev/null +++ b/apps/web/src/components/nodes/NodeAgentsPanel.tsx @@ -0,0 +1,76 @@ +"use client"; + +import Link from "next/link"; +import { Shield, Wrench } from "lucide-react"; + +interface NodeAgent { + id: string; + name: string; + kind?: string; + slug?: string; +} + +interface NodeAgentsPanelProps { + guardian?: NodeAgent | null; + steward?: NodeAgent | null; +} + +export function NodeAgentsPanel({ guardian, steward }: NodeAgentsPanelProps) { + if (!guardian && !steward) { + return null; + } + + return ( +

+

+ + Системні агенти ноди +

+ +
+ {/* Guardian Agent */} +
+
+ + Node Guardian +
+ {guardian ? ( + + {guardian.name} + + ) : ( + Не призначено + )} + {guardian?.kind && ( +

{guardian.kind}

+ )} +
+ + {/* Steward Agent */} +
+
+ + Node Steward +
+ {steward ? ( + + {steward.name} + + ) : ( + Не призначено + )} + {steward?.kind && ( +

{steward.kind}

+ )} +
+
+
+ ); +} + diff --git a/apps/web/src/lib/agent-dashboard.ts b/apps/web/src/lib/agent-dashboard.ts index 6383b98a..8e2b2e5c 100644 --- a/apps/web/src/lib/agent-dashboard.ts +++ b/apps/web/src/lib/agent-dashboard.ts @@ -153,6 +153,13 @@ export interface AgentPublicProfile { is_system?: boolean; } +export interface CityRoomSummary { + id: string; + slug: string; + name: string; + matrix_room_id?: string | null; +} + export interface AgentDashboard { profile: AgentProfile; node?: AgentNode; @@ -162,6 +169,7 @@ export interface AgentDashboard { system_prompts?: AgentSystemPrompts; public_profile?: AgentPublicProfile; microdao_memberships?: AgentMicrodaoMembership[]; + primary_city_room?: CityRoomSummary | null; } // ============================================================================ diff --git a/apps/web/src/lib/types/microdao.ts b/apps/web/src/lib/types/microdao.ts index 5216caad..34d66bd2 100644 --- a/apps/web/src/lib/types/microdao.ts +++ b/apps/web/src/lib/types/microdao.ts @@ -72,6 +72,17 @@ export interface MicrodaoCitizenView { primary_room_slug?: string | null; } +// ============================================================================= +// City Room Summary (for chat embedding) +// ============================================================================= + +export interface CityRoomSummary { + id: string; + slug: string; + name: string; + matrix_room_id?: string | null; +} + // ============================================================================= // MicroDAO Detail (for /microdao/[slug]) // ============================================================================= @@ -102,6 +113,9 @@ export interface MicrodaoDetail { agents: MicrodaoAgentView[]; channels: MicrodaoChannelView[]; public_citizens: MicrodaoCitizenView[]; + + // Primary city room for chat + primary_city_room?: CityRoomSummary | null; } // ============================================================================= diff --git a/apps/web/src/lib/types/nodes.ts b/apps/web/src/lib/types/nodes.ts index f93b1ec9..17fc5522 100644 --- a/apps/web/src/lib/types/nodes.ts +++ b/apps/web/src/lib/types/nodes.ts @@ -1,3 +1,10 @@ +export interface NodeAgentSummary { + id: string; + name: string; + kind?: string; + slug?: string; +} + export interface NodeProfile { node_id: string; name: string; @@ -9,6 +16,10 @@ export interface NodeProfile { agents_total: number; agents_online: number; last_heartbeat?: string | null; + guardian_agent_id?: string | null; + steward_agent_id?: string | null; + guardian_agent?: NodeAgentSummary | null; + steward_agent?: NodeAgentSummary | null; } export interface NodeListResponse { diff --git a/docs/internal/agents/NODE_AGENTS_INVENTORY.md b/docs/internal/agents/NODE_AGENTS_INVENTORY.md new file mode 100644 index 00000000..b6796a45 --- /dev/null +++ b/docs/internal/agents/NODE_AGENTS_INVENTORY.md @@ -0,0 +1,142 @@ +# Node Agents Inventory + +**Дата:** 28 листопада 2025 +**Статус:** ✅ Інвентаризація завершена +**Результат TASK 031_NODE_AGENTS_DISCOVERY** + +--- + +## 1. Node Monitoring Agents + +### 1.1. Monitor Agent (NODE1) + +| Поле | Значення | +|------|----------| +| **ID** | `agent-monitor-node1` | +| **Display Name** | Monitor Agent (НОДА1) | +| **Slug** | `monitor-node1` | +| **Role** | System Monitoring & Event Logging (Node-1) | +| **Model** | mistral-nemo:12b | +| **Backend** | ollama | +| **Node** | node-1-hetzner-gex44 | +| **Kind** | infra_monitor | +| **Department** | System | +| **Файл опису** | `src/api/node1Agents.ts` (рядки 76-92) | +| **Статус** | ✅ Існує в коді | + +**Функції:** +- Моніторинг CPU, RAM, GPU, Disk +- Відстеження стану сервісів (Router, Swapper, Ollama, Matrix, Postgres, NATS) +- Генерація звітів про інциденти +- Виявлення аномалій + +### 1.2. Monitor Agent (NODE2) + +| Поле | Значення | +|------|----------| +| **ID** | `agent-monitor-node2` / `monitor-node2` | +| **Display Name** | Monitor Agent (НОДА2) | +| **Slug** | `monitor-node2` | +| **Role** | System Monitoring & Event Logging (Node-2) | +| **Model** | mistral-nemo:12b | +| **Backend** | ollama | +| **Node** | node-2-macbook-m4max | +| **Kind** | infra_monitor | +| **Department** | System | +| **Файли опису** | `src/api/node2Agents.ts` (рядки 37-52), `config/agents_city_mapping.yaml`, `router-config.yml` | +| **Статус** | ✅ Існує в БД та коді | + +**Функції:** +- Аналогічні до NODE1 Monitor +- Додатково: архітектор-інспектор DAGI + +--- + +## 2. Node Steward / NodeOps Agents + +### 2.1. Node Steward (NODE1) + +| Поле | Значення | +|------|----------| +| **ID** | `node-steward-node1` (пропонується) | +| **Display Name** | Node Steward (НОДА1) | +| **Slug** | `node-steward-node1` | +| **Role** | Curator of Node Stack | +| **Model** | mistral-nemo:12b (рекомендовано) | +| **Node** | node-1-hetzner-gex44 | +| **Kind** | infra_ops | +| **Статус** | ❌ НЕ ІСНУЄ — потрібно створити | + +**Заплановані функції:** +- Інвентаризація стеку ноди +- Порівняння з DAOS стандартами +- Планування оновлень та встановлень +- Документування конфігурації + +### 2.2. Node Steward (NODE2) + +| Поле | Значення | +|------|----------| +| **ID** | `node-steward-node2` (пропонується) | +| **Display Name** | Node Steward (НОДА2) | +| **Slug** | `node-steward-node2` | +| **Role** | Curator of Node Stack | +| **Model** | mistral-nemo:12b (рекомендовано) | +| **Node** | node-2-macbook-m4max | +| **Kind** | infra_ops | +| **Статус** | ❌ НЕ ІСНУЄ — потрібно створити | + +--- + +## 3. Кандидати на офіційні ролі + +### Node Guardian (is_node_guardian = true) + +| Нода | Агент | ID | +|------|-------|-----| +| NODE1 | Monitor Agent (НОДА1) | `monitor-node1` | +| NODE2 | Monitor Agent (НОДА2) | `monitor-node2` | + +### Node Steward (is_node_steward = true) + +| Нода | Агент | ID | +|------|-------|-----| +| NODE1 | Node Steward (НОДА1) | `node-steward-node1` (створити) | +| NODE2 | Node Steward (НОДА2) | `node-steward-node2` (створити) | + +--- + +## 4. Джерела даних + +### Файли з описом агентів: + +1. `src/api/node1Agents.ts` — агенти NODE1 +2. `src/api/node2Agents.ts` — агенти NODE2 +3. `config/agents_city_mapping.yaml` — маппінг агентів на кімнати +4. `router-config.yml` — конфігурація DAGI Router +5. `docs/NODE2_AGENTS_FULL_INVENTORY.md` — повна інвентаризація NODE2 +6. `docs/users/agents/SYSTEM_AGENTS_DAIS.md` — DAIS паспорти + +### Сервіси моніторингу: + +1. `services/monitor-agent-service/` — сервіс Monitor Agent +2. `src/components/monitor/NodeMonitorChat.tsx` — UI компонент чату з Monitor + +--- + +## 5. Рекомендації для TASK 032 + +1. **Створити міграцію** з полями: + - `agents.is_node_guardian` (boolean) + - `agents.is_node_steward` (boolean) + - `node_cache.guardian_agent_id` (text) + - `node_cache.steward_agent_id` (text) + +2. **Створити агентів Node Steward** для NODE1 та NODE2 + +3. **Оновити існуючих Monitor Agent** — встановити `is_node_guardian = true` + +4. **Прив'язати агентів до нод** через `guardian_agent_id` / `steward_agent_id` + +5. **Додати в Node Dashboard UI** панель з агентами ноди + diff --git a/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md b/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md new file mode 100644 index 00000000..8ed5aa60 --- /dev/null +++ b/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md @@ -0,0 +1,97 @@ +# TASK 033: Agent & MicroDAO Chat Widgets + +**Дата:** 28 листопада 2025 +**Статус:** ✅ Завершено + +## Мета + +1. У кожному Agent Dashboard (`/agents/[id]`) має бути **діалогове вікно (Matrix-чат) з цим агентом**. +2. У кожному MicroDAO (`/microdao/[slug]`) має бути **публічний чат кімнати цього MicroDAO**, де оркестратор спілкується з користувачами. + +## Виконані зміни + +### 1. Backend: Agent Dashboard → primary_city_room + +**Файл:** `services/city-service/routes_city.py` + +- Оновлено endpoint `GET /city/agents/{id}/dashboard` +- Додано поле `primary_city_room` до відповіді +- Пріоритет визначення кімнати: + 1. Перша кімната агента з `city_rooms` + 2. Primary room MicroDAO агента (якщо є `primary_microdao_id`) + 3. `null` якщо немає + +### 2. Backend: MicroDAO Detail → primary_city_room + +**Файл:** `services/city-service/models_city.py` + +- Додано модель `CityRoomSummary`: + ```python + class CityRoomSummary(BaseModel): + id: str + slug: str + name: str + matrix_room_id: Optional[str] = None + ``` +- Оновлено `MicrodaoDetail` — додано поле `primary_city_room` + +**Файл:** `services/city-service/repo_city.py` + +- Додано функцію `get_microdao_primary_room(microdao_id)`: + - Шукає primary room MicroDAO + - Пріоритет: `room_type='primary'` → `room_type='public'` → будь-яка активна + +**Файл:** `services/city-service/routes_city.py` + +- Оновлено endpoint `GET /city/microdao/{slug}` +- Додано виклик `get_microdao_primary_room()` та заповнення `primary_city_room` + +### 3. Frontend: Типи + +**Файл:** `apps/web/src/lib/agent-dashboard.ts` + +- Додано тип `CityRoomSummary` +- Оновлено `AgentDashboard` — додано поле `primary_city_room` + +**Файл:** `apps/web/src/lib/types/microdao.ts` + +- Додано тип `CityRoomSummary` +- Оновлено `MicrodaoDetail` — додано поле `primary_city_room` + +### 4. Frontend: Agent Console (`/agents/[agentId]`) + +**Файл:** `apps/web/src/app/agents/[agentId]/page.tsx` + +- Оновлено Chat Tab: + - Прямий чат з агентом через DAGI Router (існуючий) + - Нова секція "Публічна кімната агента" з `CityChatWidget` +- Якщо `primary_city_room` є — показує Matrix-чат +- Якщо немає — показує повідомлення про необхідність налаштування + +### 5. Frontend: MicroDAO Page (`/microdao/[slug]`) + +**Файл:** `apps/web/src/app/microdao/[slug]/page.tsx` + +- Додано секцію "Публічний чат MicroDAO" +- Використовує `CityChatWidget` з `primary_city_room.slug` +- Показує інформацію про оркестратора +- Якщо кімната не налаштована — показує placeholder + +## Перевикористання + +Обидві сторінки використовують існуючий компонент `CityChatWidget` з `/components/city/CityChatWidget.tsx`, який вже працює на сторінці громадянина (`/citizens/[slug]`). + +## Acceptance Criteria + +- [x] `/agents/[id]` — секція "Публічна кімната агента" з Matrix-чатом +- [x] `/microdao/[slug]` — секція "Публічний чат MicroDAO" з Matrix-чатом +- [x] Перевикористано `CityChatWidget` +- [x] Білд проходить успішно +- [x] Типи оновлено на фронтенді та бекенді + +## Пов'язані завдання + +- **TASK 031:** Node Agents Discovery +- **TASK 032:** Node Guardian/Steward Formalize +- **Citizen Interact Layer v1:** Базовий функціонал чату для громадян + diff --git a/migrations/030_node_guardian_steward.sql b/migrations/030_node_guardian_steward.sql new file mode 100644 index 00000000..72703720 --- /dev/null +++ b/migrations/030_node_guardian_steward.sql @@ -0,0 +1,97 @@ +-- Migration 030: Node Guardian and Steward +-- Додає поля для прив'язки агентів Guardian/Steward до нод + +-- 1. Розширити таблицю agents полями для ролей Guardian/Steward +ALTER TABLE agents + ADD COLUMN IF NOT EXISTS is_node_guardian boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_node_steward boolean NOT NULL DEFAULT false; + +-- 2. Розширити node_cache полями для прив'язки агентів +ALTER TABLE node_cache + ADD COLUMN IF NOT EXISTS guardian_agent_id text, + ADD COLUMN IF NOT EXISTS steward_agent_id text; + +-- 3. Індекси для швидкого пошуку +CREATE INDEX IF NOT EXISTS idx_agents_is_node_guardian ON agents(is_node_guardian) WHERE is_node_guardian = true; +CREATE INDEX IF NOT EXISTS idx_agents_is_node_steward ON agents(is_node_steward) WHERE is_node_steward = true; + +-- 4. Оновити існуючих Monitor Agent як Guardian +UPDATE agents +SET is_node_guardian = true +WHERE id IN ('monitor-node1', 'monitor-node2', 'agent-monitor-node1', 'agent-monitor-node2'); + +-- 5. Прив'язати Guardian до нод +UPDATE node_cache +SET guardian_agent_id = 'monitor-node2' +WHERE node_id = 'node-2-macbook-m4max'; + +UPDATE node_cache +SET guardian_agent_id = 'monitor-node1' +WHERE node_id = 'node-1-hetzner-gex44'; + +-- 6. Створити агентів Node Steward (якщо ще не існують) +INSERT INTO agents ( + id, display_name, kind, status, node_id, + is_public, is_node_steward, public_slug, + created_at, updated_at +) VALUES +( + 'node-steward-node1', + 'Node Steward (НОДА1)', + 'infra_ops', + 'online', + 'node-1-hetzner-gex44', + true, + true, + 'node-steward-node1', + NOW(), + NOW() +), +( + 'node-steward-node2', + 'Node Steward (НОДА2)', + 'infra_ops', + 'online', + 'node-2-macbook-m4max', + true, + true, + 'node-steward-node2', + NOW(), + NOW() +) +ON CONFLICT (id) DO UPDATE SET + is_node_steward = true, + kind = 'infra_ops', + updated_at = NOW(); + +-- 7. Прив'язати Steward до нод +UPDATE node_cache +SET steward_agent_id = 'node-steward-node1' +WHERE node_id = 'node-1-hetzner-gex44'; + +UPDATE node_cache +SET steward_agent_id = 'node-steward-node2' +WHERE node_id = 'node-2-macbook-m4max'; + +-- 8. Переконатися, що Monitor Agent (NODE1) існує +INSERT INTO agents ( + id, display_name, kind, status, node_id, + is_public, is_node_guardian, public_slug, + created_at, updated_at +) VALUES ( + 'monitor-node1', + 'Node Monitor (НОДА1)', + 'infra_monitor', + 'online', + 'node-1-hetzner-gex44', + true, + true, + 'monitor-node1', + NOW(), + NOW() +) +ON CONFLICT (id) DO UPDATE SET + is_node_guardian = true, + kind = 'infra_monitor', + updated_at = NOW(); + diff --git a/services/city-service/models_city.py b/services/city-service/models_city.py index 810b6168..6b28d01f 100644 --- a/services/city-service/models_city.py +++ b/services/city-service/models_city.py @@ -396,6 +396,14 @@ class MicrodaoAgentView(BaseModel): is_core: bool +class CityRoomSummary(BaseModel): + """Summary of a city room for chat embedding""" + id: str + slug: str + name: str + matrix_room_id: Optional[str] = None + + class MicrodaoDetail(BaseModel): """Full MicroDAO detail view""" id: str @@ -423,6 +431,9 @@ class MicrodaoDetail(BaseModel): agents: List[MicrodaoAgentView] = [] channels: List[MicrodaoChannelView] = [] public_citizens: List[MicrodaoCitizenView] = [] + + # Primary city room for chat + primary_city_room: Optional[CityRoomSummary] = None class AgentMicrodaoMembership(BaseModel): diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py index 3c3ba078..f67337ac 100644 --- a/services/city-service/repo_city.py +++ b/services/city-service/repo_city.py @@ -1505,7 +1505,7 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]: # ============================================================================= async def get_all_nodes() -> List[dict]: - """Отримати список всіх нод з кількістю агентів""" + """Отримати список всіх нод з кількістю агентів та Guardian/Steward""" pool = await get_pool() query = """ @@ -1518,18 +1518,47 @@ async def get_all_nodes() -> List[dict]: nc.status, nc.gpu, nc.last_sync AS last_heartbeat, + nc.guardian_agent_id, + nc.steward_agent_id, (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total, - (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online, + ga.display_name AS guardian_name, + sa.display_name AS steward_name FROM node_cache nc + LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id + LEFT JOIN agents sa ON nc.steward_agent_id = sa.id ORDER BY nc.environment DESC, nc.node_name """ rows = await pool.fetch(query) - return [dict(row) for row in rows] + result = [] + for row in rows: + data = dict(row) + # Build guardian_agent object + if data.get("guardian_agent_id"): + data["guardian_agent"] = { + "id": data.get("guardian_agent_id"), + "name": data.get("guardian_name"), + } + else: + data["guardian_agent"] = None + # Build steward_agent object + if data.get("steward_agent_id"): + data["steward_agent"] = { + "id": data.get("steward_agent_id"), + "name": data.get("steward_name"), + } + else: + data["steward_agent"] = None + # Clean up + data.pop("guardian_name", None) + data.pop("steward_name", None) + result.append(data) + return result async def get_node_by_id(node_id: str) -> Optional[dict]: - """Отримати ноду по ID""" + """Отримати ноду по ID з Guardian та Steward агентами""" pool = await get_pool() query = """ @@ -1542,14 +1571,58 @@ async def get_node_by_id(node_id: str) -> Optional[dict]: nc.status, nc.gpu, nc.last_sync AS last_heartbeat, + nc.guardian_agent_id, + nc.steward_agent_id, (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total, - (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online, + -- Guardian agent info + ga.display_name AS guardian_name, + ga.kind AS guardian_kind, + ga.public_slug AS guardian_slug, + -- Steward agent info + sa.display_name AS steward_name, + sa.kind AS steward_kind, + sa.public_slug AS steward_slug FROM node_cache nc + LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id + LEFT JOIN agents sa ON nc.steward_agent_id = sa.id WHERE nc.node_id = $1 """ row = await pool.fetchrow(query, node_id) - return dict(row) if row else None + if not row: + return None + + data = dict(row) + + # Build guardian_agent object + if data.get("guardian_agent_id"): + data["guardian_agent"] = { + "id": data.get("guardian_agent_id"), + "name": data.get("guardian_name"), + "kind": data.get("guardian_kind"), + "slug": data.get("guardian_slug"), + } + else: + data["guardian_agent"] = None + + # Build steward_agent object + if data.get("steward_agent_id"): + data["steward_agent"] = { + "id": data.get("steward_agent_id"), + "name": data.get("steward_name"), + "kind": data.get("steward_kind"), + "slug": data.get("steward_slug"), + } + else: + data["steward_agent"] = None + + # Clean up intermediate fields + for key in ["guardian_name", "guardian_kind", "guardian_slug", + "steward_name", "steward_kind", "steward_slug"]: + data.pop(key, None) + + return data # ============================================================================= @@ -1656,3 +1729,38 @@ async def create_microdao_for_agent( return dict(dao_row) + +async def get_microdao_primary_room(microdao_id: str) -> Optional[dict]: + """ + Отримати основну кімнату MicroDAO для чату. + Пріоритет: primary room → перша публічна кімната → будь-яка кімната. + """ + pool = await get_pool() + + query = """ + SELECT + cr.id, + cr.slug, + cr.name, + cr.matrix_room_id + FROM city_rooms cr + WHERE cr.microdao_id = $1 + AND cr.is_active = true + ORDER BY + CASE WHEN cr.room_type = 'primary' THEN 0 + WHEN cr.room_type = 'public' THEN 1 + ELSE 2 END, + cr.created_at + LIMIT 1 + """ + + row = await pool.fetchrow(query, microdao_id) + if row: + return { + "id": str(row["id"]), + "slug": row["slug"], + "name": row["name"], + "matrix_room_id": row.get("matrix_room_id") + } + return None + diff --git a/services/city-service/routes_city.py b/services/city-service/routes_city.py index a0b77a30..94dbf8e4 100644 --- a/services/city-service/routes_city.py +++ b/services/city-service/routes_city.py @@ -1206,10 +1206,28 @@ async def get_agent_dashboard(agent_id: str): for item in memberships_raw ] + # Get primary city room for agent + primary_city_room = None + # Priority 1: agent's primary room from city_rooms + if rooms and len(rooms) > 0: + primary_room = rooms[0] # First room as primary + primary_city_room = { + "id": primary_room.get("id"), + "slug": primary_room.get("slug"), + "name": primary_room.get("name"), + "matrix_room_id": primary_room.get("matrix_room_id") + } + # Priority 2: Get from primary MicroDAO's main room + elif agent.get("primary_microdao_id"): + microdao_room = await repo_city.get_microdao_primary_room(agent["primary_microdao_id"]) + if microdao_room: + primary_city_room = microdao_room + # Build dashboard response dashboard = { "profile": profile, "node": node_info, + "primary_city_room": primary_city_room, "runtime": { "health": "healthy" if agent.get("status") == "online" else "unknown", "last_success_at": None, @@ -1466,6 +1484,18 @@ async def get_microdao_by_slug(slug: str): is_platform=child.get("is_platform", False) )) + # Get primary city room for MicroDAO + primary_city_room = await repo_city.get_microdao_primary_room(dao["id"]) + primary_room_summary = None + if primary_city_room: + from models_city import CityRoomSummary + primary_room_summary = CityRoomSummary( + id=primary_city_room["id"], + slug=primary_city_room["slug"], + name=primary_city_room["name"], + matrix_room_id=primary_city_room.get("matrix_room_id") + ) + return MicrodaoDetail( id=dao["id"], slug=dao["slug"], @@ -1483,7 +1513,8 @@ async def get_microdao_by_slug(slug: str): logo_url=dao.get("logo_url"), agents=agents, channels=channels, - public_citizens=public_citizens + public_citizens=public_citizens, + primary_city_room=primary_room_summary ) except HTTPException: