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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
76
apps/web/src/components/nodes/NodeAgentsPanel.tsx
Normal file
76
apps/web/src/components/nodes/NodeAgentsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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 {
|
||||
|
||||
142
docs/internal/agents/NODE_AGENTS_INVENTORY.md
Normal file
142
docs/internal/agents/NODE_AGENTS_INVENTORY.md
Normal 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** панель з агентами ноди
|
||||
|
||||
97
docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md
Normal file
97
docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md
Normal 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:** Базовий функціонал чату для громадян
|
||||
|
||||
97
migrations/030_node_guardian_steward.sql
Normal file
97
migrations/030_node_guardian_steward.sql
Normal 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user