feat: DAGI Router v2 - new endpoints, hooks, and UI card

This commit is contained in:
Apple
2025-12-01 05:21:43 -08:00
parent 53f31adbf0
commit e3accd4df0
221 changed files with 999 additions and 261 deletions

View File

@@ -359,3 +359,4 @@ cat docs/tasks/PHASE3_MASTER_TASK.md | pbcopy

View File

@@ -126,3 +126,4 @@ if (errorMessage.includes('Provider error') ||

View File

@@ -612,3 +612,4 @@ await knowledgeBaseService.uploadFile("helion", file);

View File

@@ -185,3 +185,4 @@ Request body: {

View File

@@ -126,3 +126,4 @@

View File

@@ -422,3 +422,4 @@ http://localhost:8899/microdao/daarion

View File

@@ -515,3 +515,4 @@ const systemPrompt = DEFAULT_PROMPTS[agentId][language];

View File

@@ -643,3 +643,4 @@ GET /api/telegram/{agent_id}/status

View File

@@ -162,3 +162,4 @@ INFO: Selected provider: LLMProvider(id='llm_local_qwen3_8b')

View File

@@ -461,3 +461,4 @@ Remaining Work:

View File

@@ -131,3 +131,4 @@

View File

@@ -171,3 +171,4 @@

View File

@@ -308,3 +308,4 @@ export function EnergyUnionCabinetPage() {

View File

@@ -149,3 +149,4 @@

View File

@@ -382,3 +382,4 @@ Helion потребує перереєстрації webhook, інші боти

View File

@@ -500,3 +500,4 @@ export function MobileResponsiveChatPage() {

View File

@@ -173,3 +173,4 @@ LLM сервіси повністю налаштовані та працюють

View File

@@ -366,3 +366,4 @@ http://localhost:8899/microdao/energy-union

View File

@@ -106,3 +106,4 @@ getMicroDaoWorkspace(microDaoId: string): Promise<Workspace | null>

View File

@@ -412,3 +412,4 @@ curl http://localhost:9500/api/agent/monitor/file-urls?agent_id=monitor

View File

@@ -163,3 +163,4 @@ curl -X POST http://localhost:9500/api/agent/monitor/chat \

View File

@@ -317,3 +317,4 @@ curl 'http://localhost:9500/api/project/changes?since=1700000000000&limit=10'

View File

@@ -247,3 +247,4 @@ location.reload();

View File

@@ -177,3 +177,4 @@ window.dispatchEvent(new CustomEvent('project-change', {

View File

@@ -368,3 +368,4 @@ localStorage.setItem(storageKey, JSON.stringify(changes.slice(0, 50)));

View File

@@ -184,3 +184,4 @@

View File

@@ -537,3 +537,4 @@ curl -X POST http://localhost:8896/api/ocr/upload \

View File

@@ -222,3 +222,4 @@ docker exec ollama ollama ps

View File

@@ -73,3 +73,4 @@ echo " 4. Протестувати Ollama з GPU: ollama run qwen3:8b 'test'"

View File

@@ -111,3 +111,4 @@ time ollama run qwen3:8b "Привіт, тест GPU"

View File

@@ -108,3 +108,4 @@ time ollama run qwen3:8b "test"

View File

@@ -81,3 +81,4 @@ echo " - Загальне CPU: 85.3% → 40-50%"

View File

@@ -628,3 +628,4 @@ const saveConversation = async () => {

View File

@@ -446,3 +446,4 @@ You now have a fully functional agent integration system. Agents can automatical

View File

@@ -381,3 +381,4 @@ Test 5: Internal Endpoints

View File

@@ -379,3 +379,4 @@ All specifications are complete. Pick a starting point:

View File

@@ -472,3 +472,4 @@ docker-compose -f docker-compose.phase3.yml down -v

View File

@@ -402,3 +402,4 @@ Sofia: "В проєкті X є 3 нові задачі:

View File

@@ -431,3 +431,4 @@ Complete Phase 4.5 fully (2-3 години) → Then start Phase 5 with real aut

View File

@@ -523,3 +523,4 @@ useAuthStore.getState().clearSession();

View File

@@ -323,3 +323,4 @@ PHASE4_PROGRESS_REPORT.md ✅ (this file)

View File

@@ -572,3 +572,4 @@ Code Quality:

View File

@@ -194,3 +194,4 @@ curl http://localhost:7011/auth/me \

View File

@@ -197,3 +197,4 @@ curl -X POST http://localhost:7011/auth/login \

View File

@@ -154,3 +154,4 @@ If agent replies, **Phase 2 works!** 🚀

View File

@@ -248,3 +248,4 @@ After Phase 3 works:

View File

@@ -322,3 +322,4 @@ curl http://144.76.224.179:9102/health

View File

@@ -411,3 +411,4 @@ cat docs/tasks/PHASE2_MASTER_TASK.md | pbcopy

View File

@@ -339,3 +339,4 @@ cd services/agent-filter

View File

@@ -193,3 +193,4 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:7014';

View File

@@ -185,3 +185,4 @@ docker-compose restart swapper-service

View File

@@ -201,3 +201,4 @@ swapper:

View File

@@ -260,3 +260,4 @@ swapper:

View File

@@ -197,3 +197,4 @@ ssh root@144.76.224.179 "cd /opt/microdao-daarion && docker-compose up -d swappe

View File

@@ -314,3 +314,4 @@ cryptography==41.0.7

View File

@@ -368,3 +368,4 @@ done

View File

@@ -602,3 +602,4 @@ async def universal_telegram_webhook(bot_id: str, update: TelegramUpdate):

View File

@@ -129,3 +129,4 @@ docker logs --tail 50 dagi-web-search-service

View File

@@ -168,3 +168,4 @@ INFO: 145.224.94.89:27620 - "POST /route HTTP/1.1" 200 OK

View File

@@ -322,3 +322,4 @@ docker ps | grep -E 'dagi-gateway|dagi-tts|dagi-stt'

View File

@@ -301,3 +301,4 @@ async def text_to_speech(text: str, voice_id: str):

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
const CITY_SERVICE_URL =
process.env.INTERNAL_API_URL ||
process.env.CITY_SERVICE_URL ||
'http://daarion-city-service:7001';
export const dynamic = 'force-dynamic';
// Fallback response when data is unavailable
const fallbackResponse = (nodeId: string) => ({
node_id: nodeId,
total: 0,
active: 0,
phantom: 0,
stale: 0,
agents: [],
});
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ nodeId: string }> }
) {
const { nodeId } = await params;
if (!nodeId) {
return NextResponse.json(
{ error: 'nodeId is required' },
{ status: 400 }
);
}
try {
const url = `${CITY_SERVICE_URL}/city/internal/node/${encodeURIComponent(nodeId)}/dagi-router/agents`;
console.log('[dagi-router/agents] Fetching:', url);
const upstream = await fetch(url, {
cache: 'no-store',
headers: {
'content-type': 'application/json',
},
});
console.log('[dagi-router/agents] Response status:', upstream.status);
if (!upstream.ok) {
return NextResponse.json(fallbackResponse(nodeId), { status: 200 });
}
const payload = await upstream.json();
return NextResponse.json(payload, { status: 200 });
} catch (error) {
console.error('[dagi-router/agents] Error:', error);
return NextResponse.json(fallbackResponse(nodeId), { status: 200 });
}
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
const CITY_SERVICE_URL =
process.env.INTERNAL_API_URL ||
process.env.CITY_SERVICE_URL ||
'http://daarion-city-service:7001';
export const dynamic = 'force-dynamic';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ nodeId: string }> }
) {
const { nodeId } = await params;
if (!nodeId) {
return NextResponse.json(
{ error: 'nodeId is required' },
{ status: 400 }
);
}
try {
const url = `${CITY_SERVICE_URL}/city/internal/node/${encodeURIComponent(nodeId)}/dagi-router/health`;
console.log('[dagi-router/health] Fetching:', url);
const upstream = await fetch(url, {
cache: 'no-store',
headers: {
'content-type': 'application/json',
},
});
console.log('[dagi-router/health] Response status:', upstream.status);
if (!upstream.ok) {
// Return fallback instead of error
return NextResponse.json({
node_id: nodeId,
status: 'down',
version: null,
agent_count: 0,
latency_ms: null,
}, { status: 200 });
}
const payload = await upstream.json();
return NextResponse.json(payload, { status: 200 });
} catch (error) {
console.error('[dagi-router/health] Error:', error);
return NextResponse.json({
node_id: nodeId,
status: 'down',
version: null,
agent_count: 0,
latency_ms: null,
}, { status: 200 });
}
}

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server';
const CITY_SERVICE_URL =
process.env.INTERNAL_API_URL ||
process.env.CITY_SERVICE_URL ||
'http://daarion-city-service:7001';
export const dynamic = 'force-dynamic';
// Fallback response when data is unavailable
const fallbackResponse = (nodeId: string) => ({
node_id: nodeId,
status: 'down',
version: null,
latency_ms: null,
router_agent_count: 0,
db_agent_count: 0,
active: 0,
phantom: 0,
stale: 0,
last_audit_at: null,
});
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ nodeId: string }> }
) {
const { nodeId } = await params;
if (!nodeId) {
return NextResponse.json(
{ error: 'nodeId is required' },
{ status: 400 }
);
}
try {
const url = `${CITY_SERVICE_URL}/city/internal/node/${encodeURIComponent(nodeId)}/dagi-router/summary`;
console.log('[dagi-router/summary] Fetching:', url);
const upstream = await fetch(url, {
cache: 'no-store',
headers: {
'content-type': 'application/json',
},
});
console.log('[dagi-router/summary] Response status:', upstream.status);
if (!upstream.ok) {
return NextResponse.json(fallbackResponse(nodeId), { status: 200 });
}
const payload = await upstream.json();
return NextResponse.json(payload, { status: 200 });
} catch (error) {
console.error('[dagi-router/summary] Error:', error);
return NextResponse.json(fallbackResponse(nodeId), { status: 200 });
}
}

View File

@@ -15,17 +15,13 @@ import {
Search,
Filter,
ChevronDown,
Download,
Upload,
Brain
Zap
} from 'lucide-react';
import {
useDAGIRouterAgents,
runDAGIAudit,
syncPhantomAgents,
markStaleAgents,
type DAGIRouterAgent
} from '@/hooks/useDAGIAudit';
useDAGIRouterSummary,
useDAGIRouterAgents,
type DagiRouterAgent
} from '@/hooks/useDAGIRouter';
interface DAGIRouterCardProps {
nodeId: string;
@@ -65,18 +61,42 @@ const STATUS_CONFIG = {
}
};
const ROUTER_STATUS_CONFIG = {
up: {
label: 'Up',
bgClass: 'bg-emerald-500/20',
textClass: 'text-emerald-400',
dotClass: 'bg-emerald-400'
},
degraded: {
label: 'Degraded',
bgClass: 'bg-amber-500/20',
textClass: 'text-amber-400',
dotClass: 'bg-amber-400'
},
down: {
label: 'Down',
bgClass: 'bg-red-500/20',
textClass: 'text-red-400',
dotClass: 'bg-red-400'
}
};
export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps) {
const { agents, summary, lastAuditAt, isLoading, error, refresh } = useDAGIRouterAgents(nodeId);
const [isRunning, setIsRunning] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [runError, setRunError] = useState<string | null>(null);
const { data: summary, error: summaryError, mutate: refreshSummary } = useDAGIRouterSummary(nodeId);
const { data: agentsData, error: agentsError, mutate: refreshAgents } = useDAGIRouterAgents(nodeId);
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
const [searchQuery, setSearchQuery] = useState('');
const [showFilterMenu, setShowFilterMenu] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const agents = agentsData?.agents || [];
const isLoading = !summary && !summaryError;
// Filter agents
const filteredAgents = useMemo(() => {
return agents.filter(agent => {
return agents.filter((agent: DagiRouterAgent) => {
// Status filter
if (statusFilter !== 'all' && agent.status !== statusFilter) {
return false;
@@ -85,61 +105,27 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
if (searchQuery) {
const query = searchQuery.toLowerCase();
return (
agent.name.toLowerCase().includes(query) ||
agent.role?.toLowerCase().includes(query) ||
agent.id.toLowerCase().includes(query)
agent.id.toLowerCase().includes(query) ||
agent.name?.toLowerCase().includes(query) ||
agent.kind?.toLowerCase().includes(query)
);
}
return true;
});
}, [agents, statusFilter, searchQuery]);
const handleRunAudit = async () => {
setIsRunning(true);
setRunError(null);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await runDAGIAudit(nodeId);
await refresh();
} catch (e) {
setRunError(e instanceof Error ? e.message : 'Failed to run audit');
await Promise.all([refreshSummary(), refreshAgents()]);
} finally {
setIsRunning(false);
}
};
const handleSyncPhantom = async () => {
const phantomIds = agents.filter(a => a.status === 'phantom').map(a => a.id);
if (phantomIds.length === 0) return;
setIsSyncing(true);
try {
await syncPhantomAgents(nodeId, phantomIds);
await runDAGIAudit(nodeId);
await refresh();
} catch (e) {
setRunError(e instanceof Error ? e.message : 'Failed to sync');
} finally {
setIsSyncing(false);
}
};
const handleMarkStale = async () => {
const staleIds = agents.filter(a => a.status === 'stale').map(a => a.id);
if (staleIds.length === 0) return;
setIsSyncing(true);
try {
await markStaleAgents(nodeId, staleIds);
await refresh();
} catch (e) {
setRunError(e instanceof Error ? e.message : 'Failed to mark stale');
} finally {
setIsSyncing(false);
setIsRefreshing(false);
}
};
// Format timestamp
const formatTime = (timestamp: string) => {
const formatTime = (timestamp: string | null) => {
if (!timestamp) return '—';
try {
const date = new Date(timestamp);
return date.toLocaleString('uk-UA', {
@@ -164,6 +150,9 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
);
};
const routerStatus = summary?.status || 'down';
const routerConfig = ROUTER_STATUS_CONFIG[routerStatus as keyof typeof ROUTER_STATUS_CONFIG] || ROUTER_STATUS_CONFIG.down;
return (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
{/* Header */}
@@ -173,66 +162,64 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
DAGI Router
</h2>
<div className="flex items-center gap-2">
{summary.phantom > 0 && (
<button
onClick={handleSyncPhantom}
disabled={isSyncing || isRunning}
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-amber-500/20 hover:bg-amber-500/30
text-amber-400 rounded-lg transition-colors text-xs disabled:opacity-50"
title="Синхронізувати phantom агентів"
>
<Download className="w-3.5 h-3.5" />
Sync {summary.phantom}
</button>
)}
{/* Router Status Badge */}
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${routerConfig.bgClass} ${routerConfig.textClass}`}>
<span className={`w-1.5 h-1.5 rounded-full ${routerConfig.dotClass}`} />
{routerConfig.label}
</span>
<button
onClick={handleRunAudit}
disabled={isRunning || isLoading || isSyncing}
onClick={handleRefresh}
disabled={isRefreshing || isLoading}
className="flex items-center gap-2 px-3 py-1.5 bg-purple-500/20 hover:bg-purple-500/30
text-purple-400 rounded-lg transition-colors text-sm disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isRunning ? 'animate-spin' : ''}`} />
{isRunning ? 'Аудит...' : 'Запустити'}
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* Error */}
{(error || runError) && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm">
<AlertTriangle className="w-4 h-4 inline mr-2" />
{runError || error?.message || 'Помилка завантаження'}
{/* Router Info */}
{summary && (
<div className="flex items-center gap-4 mb-4 text-xs text-white/50">
{summary.version && (
<span className="flex items-center gap-1">
<Zap className="w-3 h-3" />
v{summary.version}
</span>
)}
{summary.latency_ms !== null && (
<span>{summary.latency_ms}ms</span>
)}
{summary.last_audit_at && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
Audit: {formatTime(summary.last_audit_at)}
</span>
)}
</div>
)}
{/* Loading */}
{isLoading && agents.length === 0 && (
{isLoading && (
<div className="text-center py-8 text-white/40">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2" />
Завантаження...
</div>
)}
{/* No data */}
{!isLoading && agents.length === 0 && !error && (
<div className="text-center py-8 text-white/40">
{/* Router Down */}
{!isLoading && routerStatus === 'down' && (
<div className="text-center py-6 text-white/40">
<Router className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>Ще немає даних аудиту</p>
<p className="text-sm mt-1">Натисніть &quot;Запустити&quot; для аудиту</p>
<p className="text-red-400">Router недоступний</p>
<p className="text-sm mt-1">Перевірте стан DAGI Router сервісу</p>
</div>
)}
{/* Content */}
{agents.length > 0 && (
{/* Content - when router is up or degraded */}
{!isLoading && routerStatus !== 'down' && (
<>
{/* Timestamp */}
{lastAuditAt && (
<div className="flex items-center gap-2 text-xs text-white/40 mb-4">
<Clock className="w-3 h-3" />
Останній аудит: {formatTime(lastAuditAt)}
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-3 mb-4">
<button
@@ -244,7 +231,7 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
}`}
>
<div className="text-2xl font-bold text-emerald-400">
{summary.active}
{summary?.active || 0}
</div>
<div className="text-xs text-white/50">Active</div>
</button>
@@ -253,13 +240,13 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
className={`rounded-lg p-3 text-center transition-all ${
statusFilter === 'phantom'
? 'bg-amber-500/30 ring-2 ring-amber-500/50'
: summary.phantom > 0 ? 'bg-amber-500/10 hover:bg-amber-500/20' : 'bg-slate-800/50'
: (summary?.phantom || 0) > 0 ? 'bg-amber-500/10 hover:bg-amber-500/20' : 'bg-slate-800/50'
}`}
>
<div className={`text-2xl font-bold ${
summary.phantom > 0 ? 'text-amber-400' : 'text-white/40'
(summary?.phantom || 0) > 0 ? 'text-amber-400' : 'text-white/40'
}`}>
{summary.phantom}
{summary?.phantom || 0}
</div>
<div className="text-xs text-white/50">Phantom</div>
</button>
@@ -268,13 +255,13 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
className={`rounded-lg p-3 text-center transition-all ${
statusFilter === 'stale'
? 'bg-orange-500/30 ring-2 ring-orange-500/50'
: summary.stale > 0 ? 'bg-orange-500/10 hover:bg-orange-500/20' : 'bg-slate-800/50'
: (summary?.stale || 0) > 0 ? 'bg-orange-500/10 hover:bg-orange-500/20' : 'bg-slate-800/50'
}`}
>
<div className={`text-2xl font-bold ${
summary.stale > 0 ? 'text-orange-400' : 'text-white/40'
(summary?.stale || 0) > 0 ? 'text-orange-400' : 'text-white/40'
}`}>
{summary.stale}
{summary?.stale || 0}
</div>
<div className="text-xs text-white/50">Stale</div>
</button>
@@ -282,178 +269,173 @@ export function DAGIRouterCard({ nodeId, expanded = false }: DAGIRouterCardProps
{/* Source counts */}
<div className="flex justify-between text-xs text-white/40 mb-4 px-1">
<span>Router: {summary.router_total} агентів</span>
<span>System: {summary.system_total} агентів</span>
<span>Router: {summary?.router_agent_count || 0} агентів</span>
<span>DB: {summary?.db_agent_count || 0} агентів</span>
</div>
{/* Search & Filter */}
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type="text"
placeholder="Пошук агентів..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-slate-800/50 border border-white/10 rounded-lg
text-sm text-white placeholder-white/30 focus:outline-none focus:ring-2
focus:ring-purple-500/50"
/>
{agents.length > 0 && (
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type="text"
placeholder="Пошук агентів..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-slate-800/50 border border-white/10 rounded-lg
text-sm text-white placeholder-white/30 focus:outline-none focus:ring-2
focus:ring-purple-500/50"
/>
</div>
<div className="relative">
<button
onClick={() => setShowFilterMenu(!showFilterMenu)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
statusFilter !== 'all'
? 'bg-purple-500/20 border-purple-500/50 text-purple-400'
: 'bg-slate-800/50 border-white/10 text-white/60 hover:bg-slate-800'
}`}
>
<Filter className="w-4 h-4" />
<ChevronDown className="w-3 h-3" />
</button>
{showFilterMenu && (
<div className="absolute right-0 top-full mt-1 bg-slate-800 border border-white/10
rounded-lg shadow-xl z-10 min-w-[120px] py-1">
{(['all', 'active', 'phantom', 'stale'] as const).map((status) => (
<button
key={status}
onClick={() => {
setStatusFilter(status);
setShowFilterMenu(false);
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-white/5 ${
statusFilter === status ? 'text-purple-400' : 'text-white/70'
}`}
>
{status === 'all' ? 'All' : STATUS_CONFIG[status].label}
</button>
))}
</div>
)}
</div>
</div>
<div className="relative">
<button
onClick={() => setShowFilterMenu(!showFilterMenu)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
statusFilter !== 'all'
? 'bg-purple-500/20 border-purple-500/50 text-purple-400'
: 'bg-slate-800/50 border-white/10 text-white/60 hover:bg-slate-800'
}`}
>
<Filter className="w-4 h-4" />
<ChevronDown className="w-3 h-3" />
</button>
{showFilterMenu && (
<div className="absolute right-0 top-full mt-1 bg-slate-800 border border-white/10
rounded-lg shadow-xl z-10 min-w-[120px] py-1">
{(['all', 'active', 'phantom', 'stale'] as const).map((status) => (
<button
key={status}
onClick={() => {
setStatusFilter(status);
setShowFilterMenu(false);
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-white/5 ${
statusFilter === status ? 'text-purple-400' : 'text-white/70'
}`}
>
{status === 'all' ? 'All' : STATUS_CONFIG[status].label}
</button>
))}
)}
{/* Agents Table */}
{agents.length > 0 && (
<div className="overflow-hidden rounded-lg border border-white/10">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-slate-800/50 text-left">
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider">
Agent
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider hidden md:table-cell">
Runtime
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider hidden lg:table-cell">
Last Seen
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider text-right">
Cabinet
</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{filteredAgents.map((agent: DagiRouterAgent) => (
<tr
key={agent.id}
className="hover:bg-white/5 transition-colors"
>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
STATUS_CONFIG[agent.status as keyof typeof STATUS_CONFIG]?.bgClass || 'bg-slate-700'
}`}>
<Bot className={`w-4 h-4 ${
STATUS_CONFIG[agent.status as keyof typeof STATUS_CONFIG]?.textClass || 'text-white/50'
}`} />
</div>
<div>
<div className="font-medium text-white">{agent.name || agent.id}</div>
{agent.kind && (
<div className="text-xs text-white/40">{agent.kind}</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{getStatusBadge(agent.status)}
</td>
<td className="px-4 py-3 hidden md:table-cell">
<span className="text-sm text-white/60">
{agent.runtime || '—'}
</span>
</td>
<td className="px-4 py-3 hidden lg:table-cell">
<span className="text-sm text-white/50">
{formatTime(agent.last_seen_at)}
</span>
</td>
<td className="px-4 py-3 text-right">
{agent.has_db_record ? (
<Link
href={`/agents/${agent.id}`}
className="inline-flex items-center gap-1 px-2.5 py-1.5 bg-purple-500/10
hover:bg-purple-500/20 text-purple-400 rounded-lg text-xs
transition-colors"
>
Open
<ExternalLink className="w-3 h-3" />
</Link>
) : (
<span className="text-xs text-white/30"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Empty state for filtered results */}
{filteredAgents.length === 0 && agents.length > 0 && (
<div className="text-center py-8 text-white/40">
<Search className="w-6 h-6 mx-auto mb-2 opacity-50" />
<p>Немає агентів за цим фільтром</p>
</div>
)}
</div>
</div>
)}
{/* Agents Table */}
<div className="overflow-hidden rounded-lg border border-white/10">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-slate-800/50 text-left">
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider">
Agent
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider hidden md:table-cell">
Runtime
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider hidden lg:table-cell">
Last Seen
</th>
<th className="px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider text-right">
Cabinet
</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{filteredAgents.map((agent) => (
<tr
key={agent.id}
className="hover:bg-white/5 transition-colors"
>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
STATUS_CONFIG[agent.status as keyof typeof STATUS_CONFIG]?.bgClass || 'bg-slate-700'
}`}>
<Bot className={`w-4 h-4 ${
STATUS_CONFIG[agent.status as keyof typeof STATUS_CONFIG]?.textClass || 'text-white/50'
}`} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{agent.name}</span>
{agent.has_prompts && (
<span
className="text-purple-400"
title="System Prompts налаштовані"
>
<Brain className="w-3.5 h-3.5" />
</span>
)}
{!agent.has_prompts && agent.status === 'active' && (
<span
className="text-amber-400/60"
title="System Prompts відсутні"
>
<Brain className="w-3.5 h-3.5 opacity-50" />
</span>
)}
</div>
{agent.role && (
<div className="text-xs text-white/40">{agent.role}</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{getStatusBadge(agent.status)}
</td>
<td className="px-4 py-3 hidden md:table-cell">
<div className="text-sm text-white/60">
{agent.gpu && <div>{agent.gpu}</div>}
{agent.cpu && <div className="text-xs text-white/40">{agent.cpu}</div>}
</div>
</td>
<td className="px-4 py-3 hidden lg:table-cell">
<span className="text-sm text-white/50">
{agent.last_seen_at ? formatTime(agent.last_seen_at) : '—'}
</span>
</td>
<td className="px-4 py-3 text-right">
{agent.has_cabinet && agent.cabinet_slug ? (
<Link
href={`/agents/${agent.cabinet_slug}`}
className="inline-flex items-center gap-1 px-2.5 py-1.5 bg-purple-500/10
hover:bg-purple-500/20 text-purple-400 rounded-lg text-xs
transition-colors"
>
Open
<ExternalLink className="w-3 h-3" />
</Link>
) : (
<span className="text-xs text-white/30"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
{/* No agents */}
{agents.length === 0 && (
<div className="text-center py-6 text-white/40">
<Bot className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>Немає агентів у Router</p>
</div>
{/* Empty state for filtered results */}
{filteredAgents.length === 0 && agents.length > 0 && (
<div className="text-center py-8 text-white/40">
<Search className="w-6 h-6 mx-auto mb-2 opacity-50" />
<p>Немає агентів за цим фільтром</p>
</div>
)}
</div>
)}
{/* Footer stats */}
<div className="mt-4 flex items-center justify-between text-xs text-white/40">
<span>
Показано {filteredAgents.length} з {agents.length} агентів
</span>
{summary.phantom === 0 && summary.stale === 0 && (
<span className="flex items-center gap-1 text-emerald-400">
<CheckCircle className="w-3 h-3" />
Всі агенти синхронізовані
{agents.length > 0 && (
<div className="mt-4 flex items-center justify-between text-xs text-white/40">
<span>
Показано {filteredAgents.length} з {agents.length} агентів
</span>
)}
</div>
{(summary?.phantom || 0) === 0 && (summary?.stale || 0) === 0 && (
<span className="flex items-center gap-1 text-emerald-400">
<CheckCircle className="w-3 h-3" />
Всі агенти синхронізовані
</span>
)}
</div>
)}
</>
)}
</div>

View File

@@ -0,0 +1,78 @@
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
export interface DagiRouterHealth {
node_id: string;
status: 'up' | 'down' | 'degraded';
version: string | null;
agent_count: number;
latency_ms: number | null;
}
export interface DagiRouterAgent {
id: string;
name: string | null;
kind: string | null;
runtime: string | null;
node_id: string;
last_seen_at: string | null;
status: 'active' | 'phantom' | 'stale';
has_db_record: boolean;
}
export interface DagiRouterAgentsResponse {
node_id: string;
total: number;
active: number;
phantom: number;
stale: number;
agents: DagiRouterAgent[];
}
export interface DagiRouterSummary {
node_id: string;
status: 'up' | 'down' | 'degraded';
version: string | null;
latency_ms: number | null;
router_agent_count: number;
db_agent_count: number;
active: number;
phantom: number;
stale: number;
last_audit_at: string | null;
}
export function useDAGIRouterHealth(nodeId: string | null) {
return useSWR<DagiRouterHealth>(
nodeId ? `/api/node-internal/${nodeId}/dagi-router/health` : null,
fetcher,
{
refreshInterval: 30000, // Refresh every 30 seconds
revalidateOnFocus: false,
}
);
}
export function useDAGIRouterAgents(nodeId: string | null) {
return useSWR<DagiRouterAgentsResponse>(
nodeId ? `/api/node-internal/${nodeId}/dagi-router/agents` : null,
fetcher,
{
refreshInterval: 30000,
revalidateOnFocus: false,
}
);
}
export function useDAGIRouterSummary(nodeId: string | null) {
return useSWR<DagiRouterSummary>(
nodeId ? `/api/node-internal/${nodeId}/dagi-router/summary` : null,
fetcher,
{
refreshInterval: 30000,
revalidateOnFocus: false,
}
);
}

View File

@@ -78,3 +78,4 @@ networks:

View File

@@ -122,3 +122,4 @@ networks:

View File

@@ -187,3 +187,4 @@ volumes:

View File

@@ -996,3 +996,4 @@ rules:

View File

@@ -487,3 +487,4 @@ curl -X POST http://localhost:8080/api/messaging/channels \

View File

@@ -511,3 +511,4 @@ Instead of direct Matrix API:

View File

@@ -407,3 +407,4 @@ VALUES (gen_random_uuid(), '<channel-id>', 'agent:sofia', 'agent', '@sofia-agent

View File

@@ -34,3 +34,4 @@

View File

@@ -605,3 +605,4 @@ docker exec daarion-postgres psql -U postgres -d daarion \

View File

@@ -190,3 +190,4 @@ Ref: messages.matrix_event_id - matrix_events.event_id [note: 'Message ↔ Matri

View File

@@ -541,3 +541,4 @@ open http://localhost:8899/agents

View File

@@ -863,3 +863,4 @@ networks:

View File

@@ -502,3 +502,4 @@ tools:

View File

@@ -275,3 +275,4 @@ Behavior:

View File

@@ -420,3 +420,4 @@ Behavior:

View File

@@ -238,3 +238,4 @@ COMMENT ON SCHEMA public IS 'Messenger schema v1 - Matrix-aware implementation';

View File

@@ -154,3 +154,4 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -145,3 +145,4 @@ EXECUTE FUNCTION update_timestamp();

View File

@@ -79,3 +79,4 @@ http {

View File

@@ -40,3 +40,4 @@ server {

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.daarion.node-guardian</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/apple/github-projects/microdao-daarion/scripts/node-guardian-loop.py</string>
<string>--node-id=node-2-macbook-m4max</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/apple/github-projects/microdao-daarion</string>
<key>EnvironmentVariables</key>
<dict>
<key>CITY_API_URL</key>
<string>https://daarion.space/api/city</string>
<key>SWAPPER_URL</key>
<string>http://localhost:8890</string>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/node-guardian.log</string>
<key>StandardErrorPath</key>
<string>/tmp/node-guardian.error.log</string>
</dict>
</plist>

View File

@@ -23,3 +23,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7005"]

View File

@@ -293,3 +293,4 @@ curl http://localhost:7004/internal/messaging/channels/{channel_id}/context

View File

@@ -17,3 +17,4 @@ rules:

View File

@@ -161,3 +161,4 @@ async def shutdown_event():

View File

@@ -37,3 +37,4 @@ class FilterContext(BaseModel):

View File

@@ -9,3 +9,4 @@ PyYAML==6.0.1

View File

@@ -116,3 +116,4 @@ class FilterRules:

View File

@@ -23,3 +23,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7006"]

View File

@@ -406,3 +406,4 @@ curl -X POST http://localhost:7006/internal/agent-runtime/test-channel \

View File

@@ -22,3 +22,4 @@ memory:

View File

@@ -73,3 +73,4 @@ async def post_message(agent_id: str, channel_id: str, text: str) -> bool:

View File

@@ -36,3 +36,4 @@ class LLMResponse(BaseModel):

View File

@@ -74,3 +74,4 @@ pep_client = PEPClient()

Some files were not shown because too many files have changed in this diff Show More