diff --git a/.gitignore b/.gitignore index ddd6efdc..28ae3a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ parts/ sdist/ var/ diff --git a/apps/web/src/lib/agent-dashboard.ts b/apps/web/src/lib/agent-dashboard.ts new file mode 100644 index 00000000..2cfc96d5 --- /dev/null +++ b/apps/web/src/lib/agent-dashboard.ts @@ -0,0 +1,298 @@ +import type { AgentMicrodaoMembership } from "@/lib/microdao"; + +/** + * Agent Dashboard Types and API + * Based on DAIS (Decentralized AI Agent Standard) v1 + */ + +// ============================================================================ +// Types +// ============================================================================ + +export interface DAISCore { + title?: string; + bio?: string; + mission?: string; + version?: string; +} + +export interface DAISVis { + avatar_url?: string; + avatar_style?: string; + color_primary?: string; + color_secondary?: string; + lora_refs?: string[]; + second_me_id?: string; +} + +export interface DAISCog { + base_model?: string; + provider?: string; + node_id?: string; + context_window?: number; + temperature?: number; + memory?: { + type?: string; + store?: string; + collections?: string[]; + max_tokens?: number; + }; + tools_enabled?: string[]; +} + +export interface DAISAct { + matrix?: { + user_id?: string; + rooms?: string[]; + }; + tools?: string[]; + apis?: string[]; + web3?: { + wallet_address?: string; + chains?: string[]; + }; +} + +export interface DAIS { + core: DAISCore; + vis?: DAISVis; + cog?: DAISCog; + act?: DAISAct; +} + +export interface CityPresence { + primary_room_slug?: string; + district?: string; + rooms?: Array<{ + room_id: string; + slug: string; + name: string; + role?: string; + }>; +} + +export interface AgentProfile { + agent_id: string; + display_name: string; + kind: string; + status: 'online' | 'offline' | 'degraded' | 'training' | 'maintenance'; + node_id?: string; + roles: string[]; + tags: string[]; + dais: DAIS; + city_presence?: CityPresence; +} + +export interface AgentNode { + node_id: string; + status: string; + gpu?: { + name?: string; + vram_gb?: number; + }; +} + +export interface AgentRuntime { + router_endpoint?: string; + health?: string; + last_success_at?: string | null; + last_error_at?: string | null; +} + +export interface AgentMetrics { + tasks_1h?: number; + tasks_24h?: number; + errors_1h?: number; + errors_24h?: number; + avg_latency_ms_1h?: number; + success_rate_24h?: number; + tokens_24h?: number; + last_task_at?: string; +} + +export interface AgentActivity { + timestamp: string; + type: string; + room_slug?: string; + summary?: string; +} + +export interface AgentPromptView { + content: string; + version: number; + updated_at: string; + updated_by?: string; +} + +export interface AgentSystemPrompts { + core?: AgentPromptView | null; + safety?: AgentPromptView | null; + governance?: AgentPromptView | null; + tools?: AgentPromptView | null; +} + +export interface AgentPublicProfile { + is_public: boolean; + public_slug?: string | null; + public_title?: string | null; + public_tagline?: string | null; + public_skills?: string[]; + public_district?: string | null; + public_primary_room_slug?: string | null; +} + +export interface AgentDashboard { + profile: AgentProfile; + node?: AgentNode; + runtime?: AgentRuntime; + metrics?: AgentMetrics; + recent_activity?: AgentActivity[]; + system_prompts?: AgentSystemPrompts; + public_profile?: AgentPublicProfile; + microdao_memberships?: AgentMicrodaoMembership[]; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +export async function fetchAgentDashboard(agentId: string): Promise { + const response = await fetch(`/api/agents/${encodeURIComponent(agentId)}/dashboard`); + + if (!response.ok) { + throw new Error(`Failed to fetch agent dashboard: ${response.status}`); + } + + return response.json(); +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +export function getAgentStatusColor(status: string): string { + switch (status) { + case 'online': + return 'text-green-500'; + case 'training': + case 'degraded': + return 'text-yellow-500'; + case 'offline': + case 'maintenance': + return 'text-red-500'; + default: + return 'text-gray-500'; + } +} + +export function getAgentKindIcon(kind: string): string { + const icons: Record = { + orchestrator: '🎭', + coordinator: '🎯', + specialist: '🔬', + developer: '💻', + architect: '🏗️', + marketing: '📢', + finance: '💰', + security: '🛡️', + forensics: '🔍', + vision: '👁️', + research: '📚', + memory: '🧠', + web3: '⛓️', + strategic: '♟️', + mediator: '⚖️', + innovation: '💡', + civic: '🏛️', + oracle: '🔮', + builder: '🔨', + social: '💬' + }; + return icons[kind] || '🤖'; +} + +// ============================================================================ +// System Prompts API +// ============================================================================ + +export type PromptKind = 'core' | 'safety' | 'governance' | 'tools'; + +export interface UpdatePromptResult { + agent_id: string; + kind: string; + version: number; + updated_at: string; + updated_by: string; +} + +export async function updateAgentPrompt( + agentId: string, + kind: PromptKind, + content: string, + note?: string +): Promise { + const response = await fetch( + `/api/agents/${encodeURIComponent(agentId)}/prompts/${encodeURIComponent(kind)}`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, note }) + } + ); + + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.error || 'Failed to update prompt'); + } + + return response.json(); +} + +export async function getPromptHistory(agentId: string, kind: PromptKind): Promise<{ + agent_id: string; + kind: string; + history: Array<{ + version: number; + content: string; + created_at: string; + created_by: string; + note?: string; + is_active: boolean; + }>; +}> { + const response = await fetch( + `/api/agents/${encodeURIComponent(agentId)}/prompts/${encodeURIComponent(kind)}` + ); + + if (!response.ok) { + throw new Error('Failed to get prompt history'); + } + + return response.json(); +} + +// ============================================================================ +// Public Profile API +// ============================================================================ + +export async function updateAgentPublicProfile( + agentId: string, + profile: AgentPublicProfile +): Promise { + const response = await fetch( + `/api/agents/${encodeURIComponent(agentId)}/public-profile`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(profile) + } + ); + + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.detail || body?.error || 'Failed to update public profile'); + } + + return response.json(); +} + diff --git a/apps/web/src/lib/api/citizens.ts b/apps/web/src/lib/api/citizens.ts new file mode 100644 index 00000000..9553c861 --- /dev/null +++ b/apps/web/src/lib/api/citizens.ts @@ -0,0 +1,27 @@ +import type { CitizenAskResponse } from "@/lib/types/citizens"; + +export async function askCitizen( + slug: string, + params: { question: string; context?: string } +): Promise { + const res = await fetch( + `/api/public/citizens/${encodeURIComponent(slug)}/ask`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + } + ); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + throw new Error( + data?.error || data?.detail || "Не вдалося отримати відповідь від агента." + ); + } + + return data as CitizenAskResponse; +} + + diff --git a/apps/web/src/lib/api/microdao.ts b/apps/web/src/lib/api/microdao.ts new file mode 100644 index 00000000..1a647413 --- /dev/null +++ b/apps/web/src/lib/api/microdao.ts @@ -0,0 +1,69 @@ +import type { + AgentMicrodaoMembership, + MicrodaoOption, +} from "@/lib/microdao"; + +async function request(input: RequestInfo, init?: RequestInit): Promise { + const res = await fetch(input, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + const message = + (data && (data.error || data.detail || data.message)) || + "Request failed"; + throw new Error(message); + } + + return data as T; +} + +export async function fetchMicrodaoOptions(): Promise { + const data = await request<{ items?: MicrodaoOption[] }>( + "/api/microdao/options" + ); + return data.items ?? []; +} + +export async function assignAgentToMicrodao( + agentId: string, + payload: { microdao_id: string; role?: string; is_core?: boolean } +): Promise { + return request( + `/api/agents/${encodeURIComponent(agentId)}/microdao-membership`, + { + method: "PUT", + body: JSON.stringify(payload), + } + ); +} + +export async function removeAgentFromMicrodao( + agentId: string, + microdaoId: string +): Promise { + const res = await fetch( + `/api/agents/${encodeURIComponent( + agentId + )}/microdao-membership/${encodeURIComponent(microdaoId)}`, + { + method: "DELETE", + } + ); + + if (!res.ok) { + const data = await res.json().catch(() => null); + const message = + (data && (data.error || data.detail || data.message)) || + "Failed to remove MicroDAO membership"; + throw new Error(message); + } +} + + diff --git a/apps/web/src/lib/node-dashboard.ts b/apps/web/src/lib/node-dashboard.ts new file mode 100644 index 00000000..8de9a865 --- /dev/null +++ b/apps/web/src/lib/node-dashboard.ts @@ -0,0 +1,230 @@ +/** + * Node Dashboard Types and API + * Based on Node Profile Standard v1 + */ + +// ============================================================================ +// Types +// ============================================================================ + +export interface NodeInfo { + node_id: string; + name: string; + roles: string[]; + status: 'online' | 'offline' | 'degraded' | 'maintenance'; + public_hostname: string; + environment: string; + gpu: { + name: string; + vram_gb?: number; + unified_memory_gb?: number; + } | null; + modules: ModuleStatus[]; + version: string; +} + +export interface ModuleStatus { + id: string; + status: 'up' | 'down' | 'degraded' | 'unknown'; + port?: number; + error?: string; +} + +export interface InfraMetrics { + cpu_usage_pct: number; + ram: { + total_gb: number; + used_gb: number; + }; + disk: { + total_gb: number; + used_gb: number; + }; + gpus: Array<{ + name: string; + vram_gb: number; + used_gb: number; + sm_util_pct: number; + }>; + network?: { + rx_mbps: number; + tx_mbps: number; + }; +} + +export interface SwapperStatus { + status: 'up' | 'down' | 'degraded' | 'not_installed'; + endpoint: string; + latency_ms: number; + active_model?: string; + mode?: string; + storage: { + total_gb: number; + used_gb: number; + free_gb: number; + }; + models: Array<{ + name: string; + size_gb: number; + device: string; + state: string; + }>; +} + +export interface RouterStatus { + status: 'up' | 'down' | 'degraded' | 'not_installed'; + endpoint: string; + version: string; + nats_connected?: boolean; + backends: Array<{ + name: string; + status: string; + latency_ms: number; + error?: string; + }>; + metrics: { + requests_1m: number; + requests_1h: number; + error_rate_1h: number; + avg_latency_ms_1h: number; + }; +} + +export interface OllamaStatus { + status: 'up' | 'down' | 'degraded' | 'not_installed'; + endpoint: string; + latency_ms: number; + models: string[]; + error?: string; +} + +export interface ServiceStatus { + status: 'up' | 'down' | 'degraded' | 'not_installed'; + endpoint: string; + latency_ms: number; + error?: string; +} + +export interface AIServices { + swapper: SwapperStatus; + router: RouterStatus; + ollama: OllamaStatus; + services: Record; +} + +export interface AgentSummary { + total: number; + running: number; + by_kind: Record; + top: Array<{ + agent_id: string; + display_name: string; + kind: string; + status: string; + node_id?: string; + }>; +} + +export interface MatrixStatus { + enabled: boolean; + homeserver?: string; + synapse?: { + status: string; + latency_ms: number; + }; + presence_bridge?: { + status: string; + latency_ms: number; + }; +} + +export interface MonitoringStatus { + prometheus: { + url: string; + status: string; + }; + grafana: { + url: string; + status: string; + }; + logging: { + loki: { + status: string; + }; + }; +} + +export interface NodeDashboard { + node: NodeInfo; + infra: InfraMetrics; + ai: AIServices; + agents: AgentSummary; + matrix: MatrixStatus; + monitoring: MonitoringStatus; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +export async function fetchNodeDashboard(nodeId?: string): Promise { + const url = nodeId + ? `/api/node/dashboard?nodeId=${encodeURIComponent(nodeId)}` + : '/api/node/dashboard'; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch dashboard: ${response.status}`); + } + + return response.json(); +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +export function getStatusColor(status: string): string { + switch (status) { + case 'up': + case 'online': + return 'text-green-500'; + case 'degraded': + case 'busy': + return 'text-yellow-500'; + case 'down': + case 'offline': + return 'text-red-500'; + default: + return 'text-gray-500'; + } +} + +export function getStatusBgColor(status: string): string { + switch (status) { + case 'up': + case 'online': + return 'bg-green-500/20'; + case 'degraded': + case 'busy': + return 'bg-yellow-500/20'; + case 'down': + case 'offline': + return 'bg-red-500/20'; + default: + return 'bg-gray-500/20'; + } +} + +export function formatBytes(gb: number): string { + if (gb >= 1000) { + return `${(gb / 1000).toFixed(1)} TB`; + } + return `${gb.toFixed(1)} GB`; +} + +export function formatPercent(value: number): string { + return `${value.toFixed(1)}%`; +} + diff --git a/apps/web/src/lib/types/citizens.ts b/apps/web/src/lib/types/citizens.ts new file mode 100644 index 00000000..6c270dbc --- /dev/null +++ b/apps/web/src/lib/types/citizens.ts @@ -0,0 +1,67 @@ +export interface PublicCitizenSummary { + slug: string; + display_name: string; + public_title?: string | null; + public_tagline?: string | null; + avatar_url?: string | null; + kind?: string | null; + district?: string | null; + primary_room_slug?: string | null; + public_skills: string[]; + online_status?: "online" | "offline" | "unknown" | string; + status?: string | null; +} + +export interface CityPresenceRoom { + room_id?: string | null; + slug?: string | null; + name?: string | null; +} + +export interface CityPresence { + primary_room_slug?: string | null; + rooms: CityPresenceRoom[]; +} + +export interface PublicCitizenProfile { + slug: string; + display_name: string; + kind?: string | null; + public_title?: string | null; + public_tagline?: string | null; + district?: string | null; + avatar_url?: string | null; + status?: string | null; + node_id?: string | null; + public_skills: string[]; + city_presence?: CityPresence; + dais_public: Record; + interaction: Record; + metrics_public: Record; + admin_panel_url?: string | null; + microdao?: { + slug: string; + name: string; + district?: string | null; + } | null; +} + +export interface CitizenInteractionInfo { + slug: string; + display_name: string; + primary_room_slug?: string | null; + primary_room_id?: string | null; + primary_room_name?: string | null; + matrix_user_id?: string | null; + district?: string | null; + microdao_slug?: string | null; + microdao_name?: string | null; +} + +export interface CitizenAskResponse { + answer: string; + agent_display_name: string; + agent_id: string; +} + +