feat: TASK 031-033 - Node Guardian/Steward + Agent & MicroDAO Chat Widgets

TASK 031: Node Agents Discovery
- Documented existing Monitor agents (NODE1, NODE2)
- Created NODE_AGENTS_INVENTORY.md

TASK 032: Node Guardian/Steward Formalize
- Added migration 030_node_guardian_steward.sql
- Added is_node_guardian, is_node_steward to agents
- Added guardian_agent_id, steward_agent_id to node_cache
- Updated repo_city.py for guardian/steward in node queries
- Added NodeAgentsPanel component for Node Dashboard

TASK 033: Agent & MicroDAO Chat Widgets
- Added CityRoomSummary model
- Added primary_city_room to AgentDashboard API
- Added primary_city_room to MicrodaoDetail API
- Added get_microdao_primary_room() function
- Updated Agent Console with Matrix chat section
- Updated MicroDAO page with public chat section
- Reused existing CityChatWidget component
This commit is contained in:
Apple
2025-11-28 13:51:51 -08:00
parent 4d7c4b9744
commit 773a955ecc
13 changed files with 744 additions and 67 deletions

View File

@@ -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,9 +462,19 @@ export default function AgentConsolePage() {
{/* Chat Tab */}
{activeTab === 'chat' && (
<div className="space-y-6">
{/* Direct Chat with Agent via DAGI Router */}
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-cyan-400" />
Прямий чат з агентом
</h3>
<p className="text-sm text-white/50">Спілкування через DAGI Router</p>
</div>
{/* Messages */}
<div className="h-[500px] overflow-y-auto p-4 space-y-4">
<div className="h-[400px] overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-white/50 py-8">
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
@@ -527,6 +538,32 @@ export default function AgentConsolePage() {
</div>
</div>
</div>
{/* Matrix City Room Chat */}
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<h3 className="text-lg font-semibold text-white flex items-center gap-2 mb-4">
<MessageCircle className="w-5 h-5 text-purple-400" />
Публічна кімната агента
</h3>
{dashboard?.primary_city_room ? (
<div className="space-y-3">
<p className="text-sm text-white/60">
Matrix-чат у кімнаті: <span className="text-purple-400">{dashboard.primary_city_room.name}</span>
</p>
<CityChatWidget roomSlug={dashboard.primary_city_room.slug} />
</div>
) : (
<div className="text-center py-8 text-white/50">
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>Для цього агента ще не налаштована публічна кімната.</p>
<p className="text-sm mt-2">
Прив'яжіть агента до MicroDAO або створіть кімнату в City Service.
</p>
</div>
)}
</div>
</div>
)}
</div>
</div>

View File

@@ -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() {
</div>
</section>
{/* Public Chat Room */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-purple-400" />
Публічний чат MicroDAO
</h2>
{microdao.primary_city_room ? (
<div className="space-y-3">
<p className="text-sm text-slate-400">
Matrix-чат у кімнаті: <span className="text-purple-400">{microdao.primary_city_room.name}</span>
</p>
{orchestrator && (
<p className="text-xs text-slate-500">
Оркестратор: <Link href={`/agents/${orchestrator.agent_id}`} className="text-cyan-400 hover:underline">{orchestrator.display_name}</Link>
</p>
)}
<CityChatWidget roomSlug={microdao.primary_city_room.slug} />
</div>
) : (
<div className="text-center py-8 text-slate-500">
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>Для цього MicroDAO ще не налаштована публічна кімната.</p>
<p className="text-sm mt-2 text-slate-600">
Налаштуйте primary room у City Service, щоб увімкнути чат.
</p>
</div>
)}
</section>
{/* Visibility Settings (only for orchestrator) */}
{orchestrator && (
<MicrodaoVisibilityCard

View File

@@ -14,6 +14,7 @@ import {
ModulesCard,
NodeStandardComplianceCard
} from '@/components/node-dashboard';
import { NodeAgentsPanel } from '@/components/nodes/NodeAgentsPanel';
function getNodeLabel(nodeId: string): string {
if (nodeId.includes('node-1')) return 'НОДА1';
@@ -122,6 +123,11 @@ export default function NodeCabinetPage() {
</div>
<div className="space-y-6">
{/* Node Guardian & Steward Agents */}
<NodeAgentsPanel
guardian={nodeProfile?.guardian_agent}
steward={nodeProfile?.steward_agent}
/>
<NodeStandardComplianceCard node={dashboard.node} />
<MatrixCard matrix={dashboard.matrix} />
<ModulesCard modules={dashboard.node.modules} />
@@ -245,6 +251,14 @@ export default function NodeCabinetPage() {
)}
</div>
{/* Node Guardian & Steward Agents */}
<div className="mb-6">
<NodeAgentsPanel
guardian={nodeProfile?.guardian_agent}
steward={nodeProfile?.steward_agent}
/>
</div>
{/* Notice for non-NODE1 */}
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
<p className="text-amber-400 text-sm">

View File

@@ -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 (
<div className="bg-slate-800/50 border border-slate-700/50 rounded-xl p-4 space-y-3">
<h3 className="text-sm font-medium text-slate-300 flex items-center gap-2">
<Shield className="w-4 h-4 text-emerald-400" />
Системні агенти ноди
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Guardian Agent */}
<div className="bg-slate-900/50 rounded-lg p-3 border border-slate-700/30">
<div className="flex items-center gap-2 mb-1">
<Shield className="w-4 h-4 text-cyan-400" />
<span className="text-xs text-slate-400 uppercase tracking-wide">Node Guardian</span>
</div>
{guardian ? (
<Link
href={`/agents/${guardian.id}`}
className="text-sm text-white hover:text-cyan-400 transition-colors font-medium"
>
{guardian.name}
</Link>
) : (
<span className="text-sm text-slate-500 italic">Не призначено</span>
)}
{guardian?.kind && (
<p className="text-xs text-slate-500 mt-1">{guardian.kind}</p>
)}
</div>
{/* Steward Agent */}
<div className="bg-slate-900/50 rounded-lg p-3 border border-slate-700/30">
<div className="flex items-center gap-2 mb-1">
<Wrench className="w-4 h-4 text-amber-400" />
<span className="text-xs text-slate-400 uppercase tracking-wide">Node Steward</span>
</div>
{steward ? (
<Link
href={`/agents/${steward.id}`}
className="text-sm text-white hover:text-amber-400 transition-colors font-medium"
>
{steward.name}
</Link>
) : (
<span className="text-sm text-slate-500 italic">Не призначено</span>
)}
{steward?.kind && (
<p className="text-xs text-slate-500 mt-1">{steward.kind}</p>
)}
</div>
</div>
</div>
);
}

View File

@@ -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;
}
// ============================================================================

View File

@@ -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;
}
// =============================================================================

View File

@@ -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 {

View File

@@ -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** панель з агентами ноди

View File

@@ -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:** Базовий функціонал чату для громадян

View File

@@ -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();

View File

@@ -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
@@ -424,6 +432,9 @@ class MicrodaoDetail(BaseModel):
channels: List[MicrodaoChannelView] = []
public_citizens: List[MicrodaoCitizenView] = []
# Primary city room for chat
primary_city_room: Optional[CityRoomSummary] = None
class AgentMicrodaoMembership(BaseModel):
microdao_id: str

View File

@@ -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

View File

@@ -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: