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 { api, Agent, AgentInvokeResponse } from '@/lib/api';
|
||||||
import { VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
|
import { VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
|
||||||
import { updateAgentVisibility, AgentVisibilityUpdate } from '@/lib/api/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
|
// Tab types
|
||||||
type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat';
|
type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat';
|
||||||
@@ -461,70 +462,106 @@ export default function AgentConsolePage() {
|
|||||||
|
|
||||||
{/* Chat Tab */}
|
{/* Chat Tab */}
|
||||||
{activeTab === 'chat' && (
|
{activeTab === 'chat' && (
|
||||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
|
<div className="space-y-6">
|
||||||
{/* Messages */}
|
{/* Direct Chat with Agent via DAGI Router */}
|
||||||
<div className="h-[500px] overflow-y-auto p-4 space-y-4">
|
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden">
|
||||||
{messages.length === 0 && (
|
<div className="p-4 border-b border-white/10">
|
||||||
<div className="text-center text-white/50 py-8">
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||||
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
<MessageSquare className="w-5 h-5 text-cyan-400" />
|
||||||
<p>Start a conversation with {profile?.display_name || agentId}</p>
|
Прямий чат з агентом
|
||||||
</div>
|
</h3>
|
||||||
)}
|
<p className="text-sm text-white/50">Спілкування через DAGI Router</p>
|
||||||
{messages.map(msg => (
|
</div>
|
||||||
<div
|
|
||||||
key={msg.id}
|
{/* Messages */}
|
||||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
<div className="h-[400px] overflow-y-auto p-4 space-y-4">
|
||||||
>
|
{messages.length === 0 && (
|
||||||
<div
|
<div className="text-center text-white/50 py-8">
|
||||||
className={`max-w-[80%] p-3 rounded-xl ${
|
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
msg.role === 'user'
|
<p>Start a conversation with {profile?.display_name || agentId}</p>
|
||||||
? 'bg-cyan-500/20 text-white'
|
|
||||||
: 'bg-white/10 text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
|
||||||
{msg.meta && (
|
|
||||||
<div className="mt-2 text-xs text-white/30 flex gap-2">
|
|
||||||
{msg.meta.latency_ms && <span>{msg.meta.latency_ms}ms</span>}
|
|
||||||
{msg.meta.tokens_out && <span>{msg.meta.tokens_out} tokens</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
{messages.map(msg => (
|
||||||
{invoking && (
|
<div
|
||||||
<div className="flex justify-start">
|
key={msg.id}
|
||||||
<div className="bg-white/10 p-3 rounded-xl">
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Loader2 className="w-4 h-4 text-cyan-500 animate-spin" />
|
<div
|
||||||
<span className="text-white/50">Thinking...</span>
|
className={`max-w-[80%] p-3 rounded-xl ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-cyan-500/20 text-white'
|
||||||
|
: 'bg-white/10 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||||
|
{msg.meta && (
|
||||||
|
<div className="mt-2 text-xs text-white/30 flex gap-2">
|
||||||
|
{msg.meta.latency_ms && <span>{msg.meta.latency_ms}ms</span>}
|
||||||
|
{msg.meta.tokens_out && <span>{msg.meta.tokens_out} tokens</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
{invoking && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-white/10 p-3 rounded-xl">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 text-cyan-500 animate-spin" />
|
||||||
|
<span className="text-white/50">Thinking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="border-t border-white/10 p-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => 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}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={!input.trim() || invoking}
|
||||||
|
className="px-4 py-3 bg-cyan-500 hover:bg-cyan-400 disabled:bg-white/10 disabled:text-white/30 text-white rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Matrix City Room Chat */}
|
||||||
<div className="border-t border-white/10 p-4">
|
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
|
||||||
<div className="flex gap-2">
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2 mb-4">
|
||||||
<input
|
<MessageCircle className="w-5 h-5 text-purple-400" />
|
||||||
type="text"
|
Публічна кімната агента
|
||||||
value={input}
|
</h3>
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()}
|
{dashboard?.primary_city_room ? (
|
||||||
placeholder="Type a message..."
|
<div className="space-y-3">
|
||||||
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"
|
<p className="text-sm text-white/60">
|
||||||
disabled={invoking}
|
Matrix-чат у кімнаті: <span className="text-purple-400">{dashboard.primary_city_room.name}</span>
|
||||||
/>
|
</p>
|
||||||
<button
|
<CityChatWidget roomSlug={dashboard.primary_city_room.slug} />
|
||||||
onClick={handleSendMessage}
|
</div>
|
||||||
disabled={!input.trim() || invoking}
|
) : (
|
||||||
className="px-4 py-3 bg-cyan-500 hover:bg-cyan-400 disabled:bg-white/10 disabled:text-white/30 text-white rounded-xl transition-colors"
|
<div className="text-center py-8 text-white/50">
|
||||||
>
|
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
||||||
Send
|
<p>Для цього агента ще не налаштована публічна кімната.</p>
|
||||||
</button>
|
<p className="text-sm mt-2">
|
||||||
</div>
|
Прив'яжіть агента до MicroDAO або створіть кімнату в City Service.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import Link from "next/link";
|
|||||||
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
|
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
|
||||||
import { DISTRICT_COLORS } from "@/lib/microdao";
|
import { DISTRICT_COLORS } from "@/lib/microdao";
|
||||||
import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard";
|
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() {
|
export default function MicrodaoDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -372,6 +373,36 @@ export default function MicrodaoDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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) */}
|
{/* Visibility Settings (only for orchestrator) */}
|
||||||
{orchestrator && (
|
{orchestrator && (
|
||||||
<MicrodaoVisibilityCard
|
<MicrodaoVisibilityCard
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ModulesCard,
|
ModulesCard,
|
||||||
NodeStandardComplianceCard
|
NodeStandardComplianceCard
|
||||||
} from '@/components/node-dashboard';
|
} from '@/components/node-dashboard';
|
||||||
|
import { NodeAgentsPanel } from '@/components/nodes/NodeAgentsPanel';
|
||||||
|
|
||||||
function getNodeLabel(nodeId: string): string {
|
function getNodeLabel(nodeId: string): string {
|
||||||
if (nodeId.includes('node-1')) return 'НОДА1';
|
if (nodeId.includes('node-1')) return 'НОДА1';
|
||||||
@@ -122,6 +123,11 @@ export default function NodeCabinetPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Node Guardian & Steward Agents */}
|
||||||
|
<NodeAgentsPanel
|
||||||
|
guardian={nodeProfile?.guardian_agent}
|
||||||
|
steward={nodeProfile?.steward_agent}
|
||||||
|
/>
|
||||||
<NodeStandardComplianceCard node={dashboard.node} />
|
<NodeStandardComplianceCard node={dashboard.node} />
|
||||||
<MatrixCard matrix={dashboard.matrix} />
|
<MatrixCard matrix={dashboard.matrix} />
|
||||||
<ModulesCard modules={dashboard.node.modules} />
|
<ModulesCard modules={dashboard.node.modules} />
|
||||||
@@ -245,6 +251,14 @@ export default function NodeCabinetPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Node Guardian & Steward Agents */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<NodeAgentsPanel
|
||||||
|
guardian={nodeProfile?.guardian_agent}
|
||||||
|
steward={nodeProfile?.steward_agent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Notice for non-NODE1 */}
|
{/* Notice for non-NODE1 */}
|
||||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
|
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
|
||||||
<p className="text-amber-400 text-sm">
|
<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;
|
is_system?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CityRoomSummary {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
matrix_room_id?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentDashboard {
|
export interface AgentDashboard {
|
||||||
profile: AgentProfile;
|
profile: AgentProfile;
|
||||||
node?: AgentNode;
|
node?: AgentNode;
|
||||||
@@ -162,6 +169,7 @@ export interface AgentDashboard {
|
|||||||
system_prompts?: AgentSystemPrompts;
|
system_prompts?: AgentSystemPrompts;
|
||||||
public_profile?: AgentPublicProfile;
|
public_profile?: AgentPublicProfile;
|
||||||
microdao_memberships?: AgentMicrodaoMembership[];
|
microdao_memberships?: AgentMicrodaoMembership[];
|
||||||
|
primary_city_room?: CityRoomSummary | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ export interface MicrodaoCitizenView {
|
|||||||
primary_room_slug?: string | null;
|
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])
|
// MicroDAO Detail (for /microdao/[slug])
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -102,6 +113,9 @@ export interface MicrodaoDetail {
|
|||||||
agents: MicrodaoAgentView[];
|
agents: MicrodaoAgentView[];
|
||||||
channels: MicrodaoChannelView[];
|
channels: MicrodaoChannelView[];
|
||||||
public_citizens: MicrodaoCitizenView[];
|
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 {
|
export interface NodeProfile {
|
||||||
node_id: string;
|
node_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -9,6 +16,10 @@ export interface NodeProfile {
|
|||||||
agents_total: number;
|
agents_total: number;
|
||||||
agents_online: number;
|
agents_online: number;
|
||||||
last_heartbeat?: string | null;
|
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 {
|
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
|
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):
|
class MicrodaoDetail(BaseModel):
|
||||||
"""Full MicroDAO detail view"""
|
"""Full MicroDAO detail view"""
|
||||||
id: str
|
id: str
|
||||||
@@ -424,6 +432,9 @@ class MicrodaoDetail(BaseModel):
|
|||||||
channels: List[MicrodaoChannelView] = []
|
channels: List[MicrodaoChannelView] = []
|
||||||
public_citizens: List[MicrodaoCitizenView] = []
|
public_citizens: List[MicrodaoCitizenView] = []
|
||||||
|
|
||||||
|
# Primary city room for chat
|
||||||
|
primary_city_room: Optional[CityRoomSummary] = None
|
||||||
|
|
||||||
|
|
||||||
class AgentMicrodaoMembership(BaseModel):
|
class AgentMicrodaoMembership(BaseModel):
|
||||||
microdao_id: str
|
microdao_id: str
|
||||||
|
|||||||
@@ -1505,7 +1505,7 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
async def get_all_nodes() -> List[dict]:
|
async def get_all_nodes() -> List[dict]:
|
||||||
"""Отримати список всіх нод з кількістю агентів"""
|
"""Отримати список всіх нод з кількістю агентів та Guardian/Steward"""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
@@ -1518,18 +1518,47 @@ async def get_all_nodes() -> List[dict]:
|
|||||||
nc.status,
|
nc.status,
|
||||||
nc.gpu,
|
nc.gpu,
|
||||||
nc.last_sync AS last_heartbeat,
|
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) 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
|
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
|
ORDER BY nc.environment DESC, nc.node_name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rows = await pool.fetch(query)
|
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]:
|
async def get_node_by_id(node_id: str) -> Optional[dict]:
|
||||||
"""Отримати ноду по ID"""
|
"""Отримати ноду по ID з Guardian та Steward агентами"""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
@@ -1542,14 +1571,58 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
|||||||
nc.status,
|
nc.status,
|
||||||
nc.gpu,
|
nc.gpu,
|
||||||
nc.last_sync AS last_heartbeat,
|
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) 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
|
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
|
WHERE nc.node_id = $1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
row = await pool.fetchrow(query, node_id)
|
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)
|
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
|
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
|
# Build dashboard response
|
||||||
dashboard = {
|
dashboard = {
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"node": node_info,
|
"node": node_info,
|
||||||
|
"primary_city_room": primary_city_room,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"health": "healthy" if agent.get("status") == "online" else "unknown",
|
"health": "healthy" if agent.get("status") == "online" else "unknown",
|
||||||
"last_success_at": None,
|
"last_success_at": None,
|
||||||
@@ -1466,6 +1484,18 @@ async def get_microdao_by_slug(slug: str):
|
|||||||
is_platform=child.get("is_platform", False)
|
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(
|
return MicrodaoDetail(
|
||||||
id=dao["id"],
|
id=dao["id"],
|
||||||
slug=dao["slug"],
|
slug=dao["slug"],
|
||||||
@@ -1483,7 +1513,8 @@ async def get_microdao_by_slug(slug: str):
|
|||||||
logo_url=dao.get("logo_url"),
|
logo_url=dao.get("logo_url"),
|
||||||
agents=agents,
|
agents=agents,
|
||||||
channels=channels,
|
channels=channels,
|
||||||
public_citizens=public_citizens
|
public_citizens=public_citizens,
|
||||||
|
primary_city_room=primary_room_summary
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|||||||
Reference in New Issue
Block a user