import { COOKIE_NAME } from "@shared/const"; import { z } from "zod"; import { getDb } from "./db"; import { getSessionCookieOptions } from "./_core/cookies"; import { systemRouter } from "./_core/systemRouter"; import { publicProcedure, router, protectedProcedure } from "./_core/trpc"; import { retryWithBackoff, isRetryableError, logRetryAttempt, DEFAULT_RETRY_CONFIG, } from "./chat-resilience"; import { checkOllamaHealth, listModels, chatCompletion } from "./ollama"; import { checkGatewayHealth, gatewayChat, getGatewayOrchestratorConfig, getGatewayModels, getGatewayTools, executeGatewayTool, isGatewayAvailable, getGatewayNodes, getGatewayNodeStats, } from "./gateway-proxy"; // Shared system user id for non-authenticated agent management const SYSTEM_USER_ID = 1; export const appRouter = router({ system: systemRouter, auth: router({ me: publicProcedure.query(opts => opts.ctx.user), logout: publicProcedure.mutation(({ ctx }) => { const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 }); return { success: true } as const; }), }), /** * Ollama API — серверный прокси для безопасного доступа * Приоритет: Go Gateway → прямой Ollama */ ollama: router({ health: publicProcedure.query(async () => { // Try Go Gateway first, fall back to direct Ollama const gwHealth = await checkGatewayHealth(); if (gwHealth.connected) { return { connected: true as const, latencyMs: gwHealth.latencyMs, source: "gateway" as const, llm: gwHealth.llm, error: undefined as string | undefined, }; } // Fallback: direct Ollama const ollamaHealth = await checkOllamaHealth(); return { ...ollamaHealth, source: "direct" as const }; }), models: publicProcedure.query(async () => { // Try Go Gateway first const gwModels = await getGatewayModels(); if (gwModels) { return { success: true as const, models: gwModels.data ?? [], source: "gateway" as const, }; } // Fallback: direct Ollama try { const result = await listModels(); return { success: true as const, models: result.data ?? [], source: "direct" as const, }; } catch (err: any) { return { success: false as const, models: [], error: err.message, }; } }), chat: publicProcedure .input( z.object({ model: z.string(), messages: z.array( z.object({ role: z.enum(["system", "user", "assistant"]), content: z.string(), }) ), temperature: z.number().optional(), max_tokens: z.number().optional(), }) ) .mutation(async ({ input }) => { try { const result = await chatCompletion(input.model, input.messages, { temperature: input.temperature, max_tokens: input.max_tokens, }); return { success: true as const, response: result.choices[0]?.message?.content ?? "", model: result.model, usage: result.usage, }; } catch (err: any) { return { success: false as const, response: "", error: err.message, }; } }), }), /** * Agents — управление AI-агентами (public — внутренний инструмент) */ agents: router({ list: publicProcedure.query(async () => { // getAllAgents returns both system agents (userId=0) and user-created agents const { getAllAgents } = await import("./agents"); return getAllAgents(); }), get: publicProcedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { const { getAgentById } = await import("./agents"); return getAgentById(input.id); }), create: publicProcedure .input( z.object({ name: z.string().min(1), description: z.string().optional(), role: z.string(), model: z.string(), provider: z.string(), temperature: z.number().min(0).max(2).default(0.7), maxTokens: z.number().default(2048), topP: z.number().min(0).max(1).default(1.0), frequencyPenalty: z.number().min(-2).max(2).default(0.0), presencePenalty: z.number().min(-2).max(2).default(0.0), systemPrompt: z.string().optional(), allowedTools: z.array(z.string()).default([]), allowedDomains: z.array(z.string()).default([]), tags: z.array(z.string()).default([]), }) ) .mutation(async ({ input }) => { const { createAgent } = await import("./agents"); return createAgent(SYSTEM_USER_ID, { ...input, temperature: input.temperature.toString(), topP: input.topP.toString(), frequencyPenalty: input.frequencyPenalty.toString(), presencePenalty: input.presencePenalty.toString(), } as any); }), update: publicProcedure .input( z.object({ id: z.number(), name: z.string().optional(), description: z.string().optional(), model: z.string().optional(), provider: z.string().optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().optional(), topP: z.number().min(0).max(1).optional(), frequencyPenalty: z.number().min(-2).max(2).optional(), presencePenalty: z.number().min(-2).max(2).optional(), systemPrompt: z.string().optional(), allowedTools: z.array(z.string()).optional(), allowedDomains: z.array(z.string()).optional(), isActive: z.boolean().optional(), tags: z.array(z.string()).optional(), }) ) .mutation(async ({ input }) => { const { updateAgent } = await import("./agents"); const { id, temperature, topP, frequencyPenalty, presencePenalty, ...rest } = input; const updates: Record = { ...rest }; if (temperature !== undefined) updates.temperature = temperature.toString(); if (topP !== undefined) updates.topP = topP.toString(); if (frequencyPenalty !== undefined) updates.frequencyPenalty = frequencyPenalty.toString(); if (presencePenalty !== undefined) updates.presencePenalty = presencePenalty.toString(); return updateAgent(id, updates as any); }), delete: publicProcedure .input(z.object({ id: z.number() })) .mutation(async ({ input }) => { const { deleteAgent } = await import("./agents"); return deleteAgent(input.id); }), stats: publicProcedure .input(z.object({ id: z.number(), hoursBack: z.number().default(24) })) .query(async ({ input }) => { const { getAgentStats } = await import("./agents"); return getAgentStats(input.id, input.hoursBack); }), metrics: publicProcedure .input(z.object({ id: z.number(), hoursBack: z.number().default(24) })) .query(async ({ input }) => { const { getAgentMetrics } = await import("./agents"); return getAgentMetrics(input.id, input.hoursBack); }), history: publicProcedure .input(z.object({ id: z.number(), limit: z.number().default(50) })) .query(async ({ input }) => { const { getAgentHistory } = await import("./agents"); return getAgentHistory(input.id, input.limit); }), accessControl: publicProcedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { const { getAgentAccessControl } = await import("./agents"); return getAgentAccessControl(input.id); }), updateToolAccess: publicProcedure .input( z.object({ agentId: z.number(), tool: z.string(), isAllowed: z.boolean(), maxExecutionsPerHour: z.number().optional(), timeoutSeconds: z.number().optional(), allowedPatterns: z.array(z.string()).optional(), blockedPatterns: z.array(z.string()).optional(), }) ) .mutation(async ({ input }) => { const { updateToolAccess } = await import("./agents"); const { agentId, ...updates } = input; return updateToolAccess(agentId, input.tool, updates); }), /** * Deploy an agent as a Docker Swarm container. * Creates a Swarm service with the agent-worker binary. */ deployContainer: publicProcedure .input(z.object({ agentId: z.number() })) .mutation(async ({ input }) => { const { deployAgentContainer } = await import("./agents"); return deployAgentContainer(input.agentId); }), /** * Stop and remove an agent's Docker Swarm container. */ stopContainer: publicProcedure .input(z.object({ agentId: z.number() })) .mutation(async ({ input }) => { const { stopAgentContainer } = await import("./agents"); return stopAgentContainer(input.agentId); }), /** * Get container status for an agent (checks Docker Swarm). */ containerStatus: publicProcedure .input(z.object({ agentId: z.number() })) .query(async ({ input }) => { const { getAgentContainerStatus } = await import("./agents"); return getAgentContainerStatus(input.agentId); }), /** * Chat with a specific agent using its configuration */ chat: publicProcedure .input( z.object({ agentId: z.number(), message: z.string(), conversationId: z.string().optional(), }) ) .mutation(async ({ input }) => { const { getAgentById, saveHistory } = await import("./agents"); const agent = await getAgentById(input.agentId); if (!agent) { return { success: false as const, response: "", error: "Agent not found", }; } const messages: Array<{ role: "system" | "user" | "assistant"; content: string; }> = []; if (agent.systemPrompt) { messages.push({ role: "system", content: agent.systemPrompt }); } messages.push({ role: "user", content: input.message }); const startTime = Date.now(); try { const result = await chatCompletion(agent.model, messages, { temperature: agent.temperature ? parseFloat(agent.temperature as string) : 0.7, max_tokens: agent.maxTokens ?? 2048, }); const processingTimeMs = Date.now() - startTime; const response = result.choices[0]?.message?.content ?? ""; // Save to history await saveHistory(input.agentId, { userMessage: input.message, agentResponse: response, conversationId: input.conversationId, status: "success", }); return { success: true as const, response, model: result.model, usage: result.usage, processingTimeMs, }; } catch (err: any) { await saveHistory(input.agentId, { userMessage: input.message, agentResponse: null, conversationId: input.conversationId, status: "error", }); return { success: false as const, response: "", error: err.message, }; } }), }), /** * Tools — управление инструментами агентов */ tools: router({ list: publicProcedure.query(async () => { const { getAllTools } = await import("./tools"); return getAllTools(); }), execute: publicProcedure .input( z.object({ agentId: z.number(), tool: z.string(), params: z.record(z.string(), z.unknown()), }) ) .mutation(async ({ input }) => { const { executeTool } = await import("./tools"); return executeTool(input.agentId, input.tool, input.params); }), }), /** * Browser Agent — управление браузерными сессиями через Puppeteer */ browser: router({ createSession: publicProcedure .input(z.object({ agentId: z.number() })) .mutation(async ({ input }) => { const { createBrowserSession } = await import("./browser-agent"); return createBrowserSession(input.agentId); }), execute: publicProcedure .input( z.object({ sessionId: z.string(), action: z.object({ type: z.enum([ "navigate", "click", "type", "extract", "screenshot", "scroll", "wait", "evaluate", "close", ]), params: z.record(z.string(), z.unknown()), }), }) ) .mutation(async ({ input }) => { const { executeBrowserAction } = await import("./browser-agent"); return executeBrowserAction(input.sessionId, input.action as any); }), getSessions: publicProcedure .input(z.object({ agentId: z.number() })) .query(async ({ input }) => { const { getAgentSessions } = await import("./browser-agent"); return getAgentSessions(input.agentId); }), closeSession: publicProcedure .input(z.object({ sessionId: z.string() })) .mutation(async ({ input }) => { const { executeBrowserAction } = await import("./browser-agent"); return executeBrowserAction(input.sessionId, { type: "close", params: {}, }); }), closeAllSessions: publicProcedure .input(z.object({ agentId: z.number() })) .mutation(async ({ input }) => { const { closeAllAgentSessions } = await import("./browser-agent"); await closeAllAgentSessions(input.agentId); return { success: true }; }), }), /** * Tool Builder — генерация и установка новых инструментов через LLM */ toolBuilder: router({ generate: publicProcedure .input( z.object({ name: z.string().min(1), description: z.string().min(10), category: z.string().optional(), exampleInput: z.string().optional(), exampleOutput: z.string().optional(), dangerous: z.boolean().optional(), }) ) .mutation(async ({ input }) => { const { generateTool } = await import("./tool-builder"); return generateTool(input); }), install: publicProcedure .input( z.object({ toolId: z.string(), name: z.string(), description: z.string(), category: z.string(), dangerous: z.boolean(), parameters: z.record( z.string(), z.object({ type: z.string(), description: z.string(), required: z.boolean().optional(), }) ), implementation: z.string(), }) ) .mutation(async ({ input }) => { const { installTool } = await import("./tool-builder"); return installTool(input); }), listCustom: publicProcedure.query(async () => { const { getCustomTools } = await import("./tool-builder"); return getCustomTools(); }), delete: publicProcedure .input(z.object({ toolId: z.string() })) .mutation(async ({ input }) => { const { deleteTool } = await import("./tool-builder"); return deleteTool(input.toolId); }), test: publicProcedure .input( z.object({ toolId: z.string(), params: z.record(z.string(), z.unknown()), }) ) .mutation(async ({ input }) => { const { testTool } = await import("./tool-builder"); return testTool(input.toolId, input.params); }), }), /** * Agent Compiler — компиляция агентов по ТЗ через LLM */ agentCompiler: router({ compile: publicProcedure .input( z.object({ specification: z.string().min(20), name: z.string().optional(), preferredProvider: z.string().optional(), preferredModel: z.string().optional(), }) ) .mutation(async ({ input }) => { const { compileAgentConfig } = await import("./agent-compiler"); return compileAgentConfig({ ...input, userId: SYSTEM_USER_ID }); }), deploy: publicProcedure .input( z.object({ config: z.object({ name: z.string(), description: z.string(), role: z.string(), model: z.string(), provider: z.string(), temperature: z.number(), maxTokens: z.number(), topP: z.number(), frequencyPenalty: z.number(), presencePenalty: z.number(), systemPrompt: z.string(), allowedTools: z.array(z.string()), allowedDomains: z.array(z.string()), maxRequestsPerHour: z.number(), tags: z.array(z.string()), reasoning: z.string(), }), }) ) .mutation(async ({ input }) => { const { deployCompiledAgent } = await import("./agent-compiler"); return deployCompiledAgent(input.config, SYSTEM_USER_ID); }), compileAndDeploy: publicProcedure .input( z.object({ specification: z.string().min(20), name: z.string().optional(), preferredProvider: z.string().optional(), preferredModel: z.string().optional(), }) ) .mutation(async ({ input }) => { const { compileAndDeployAgent } = await import("./agent-compiler"); return compileAndDeployAgent({ ...input, userId: SYSTEM_USER_ID }); }), }), /** * Orchestrator — main AI agent with tool-use loop * Приоритет: Go Gateway → Node.js fallback */ orchestrator: router({ // Get orchestrator config — Go Gateway first, then Node.js DB getConfig: publicProcedure.query(async () => { const gwConfig = await getGatewayOrchestratorConfig(); if (gwConfig) { return { ...gwConfig, source: "gateway" as const }; } // Fallback: Node.js orchestrator reads from DB directly const { getOrchestratorConfig } = await import("./orchestrator"); const config = await getOrchestratorConfig(); return { ...config, source: "direct" as const }; }), chat: publicProcedure .input( z.object({ messages: z.array( z.object({ role: z.enum(["user", "assistant", "system"]), content: z.string(), }) ), model: z.string().optional(), maxIterations: z.number().min(1).max(20).optional(), }) ) .mutation(async ({ input }) => { // Wrap chat with retry logic for resilience return retryWithBackoff( async () => { // Try Go Gateway first (preferred — full Go tool-use loop) const gwAvailable = await isGatewayAvailable(); if (gwAvailable) { const result = await gatewayChat( input.messages, input.model, input.maxIterations ?? 10 ); return { ...result, source: "gateway" as const }; } // Fallback: Node.js orchestrator const { orchestratorChat } = await import("./orchestrator"); const result = await orchestratorChat( input.messages, input.model, input.maxIterations ?? 10 ); return { ...result, source: "direct" as const }; }, DEFAULT_RETRY_CONFIG, (attempt, error) => { if (isRetryableError(error)) { logRetryAttempt(attempt, error, { messageCount: input.messages.length, }); } else { // Non-retryable error, throw immediately throw error; } } ); }), // List available tools — Go Gateway first tools: publicProcedure.query(async () => { const gwTools = await getGatewayTools(); if (gwTools) { return gwTools.map(t => ({ name: t.name, description: t.description, parameters: t.parameters, source: "gateway" as const, })); } // Fallback: Node.js tool definitions const { ORCHESTRATOR_TOOLS } = await import("./orchestrator"); return ORCHESTRATOR_TOOLS.map(t => ({ name: t.function.name, description: t.function.description, parameters: t.function.parameters, source: "direct" as const, })); }), // Gateway health check gatewayHealth: publicProcedure.query(async () => { return checkGatewayHealth(); }), // Execute a single tool via Go Gateway executeTool: publicProcedure .input( z.object({ tool: z.string(), args: z.record(z.string(), z.unknown()), }) ) .mutation(async ({ input }) => { return executeGatewayTool(input.tool, input.args); }), }), /** * Dashboard — aggregated real-time stats for the top status bar */ dashboard: router({ /** * Returns aggregated cluster stats: * - uptime: server process uptime formatted as "Xd Yh Zm" * - nodes: running container count from Docker * - agents: active agent count from DB * - cpu: average CPU% across all containers * - mem: total RAM used in MB */ stats: publicProcedure.query(async () => { // 1. Server uptime const uptimeSec = Math.floor(process.uptime()); const days = Math.floor(uptimeSec / 86400); const hours = Math.floor((uptimeSec % 86400) / 3600); const mins = Math.floor((uptimeSec % 3600) / 60); const uptime = days > 0 ? `${days}d ${hours}h ${mins}m` : hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; // 2. Container / node stats from Go Gateway const [nodesResult, statsResult] = await Promise.allSettled([ getGatewayNodes(), getGatewayNodeStats(), ]); const nodes = nodesResult.status === "fulfilled" && nodesResult.value ? nodesResult.value : null; const statsData = statsResult.status === "fulfilled" && statsResult.value ? statsResult.value : null; const containerCount = nodes?.containers?.length ?? nodes?.count ?? 0; const totalContainers = nodes?.containers?.length ?? 0; // CPU: average across all containers const cpuPct = statsData?.stats?.length ? statsData.stats.reduce((sum, s) => sum + s.cpuPct, 0) / statsData.stats.length : 0; // MEM: total used MB const memUseMB = statsData?.stats?.length ? statsData.stats.reduce((sum, s) => sum + s.memUseMB, 0) : 0; // 3. Active agents from DB let activeAgents = 0; try { const db = await getDb(); if (db) { const { agents } = await import("../drizzle/schema"); const { count: drizzleCount, eq } = await import("drizzle-orm"); const [{ value }] = await db .select({ value: drizzleCount() }) .from(agents) .where(eq(agents.isActive, true)); activeAgents = Number(value); } } catch { // non-fatal } return { uptime, nodes: `${containerCount} / ${totalContainers || containerCount}`, agents: activeAgents, cpuPct: Math.round(cpuPct * 10) / 10, memUseMB: Math.round(memUseMB), gatewayOnline: !!nodes, fetchedAt: new Date().toISOString(), }; }), }), /** * Nodes — Docker Swarm / standalone Docker monitoring via Go Gateway */ nodes: router({ /** * List all Swarm nodes (or standalone Docker host if Swarm not active). * Returns node info: hostname, role, status, resources, labels, etc. */ list: publicProcedure.query(async () => { const result = await getGatewayNodes(); if (!result) { return { nodes: [] as import("./gateway-proxy").GatewayNodeInfo[], count: 0, swarmActive: false, fetchedAt: new Date().toISOString(), error: "Gateway unavailable — is the Go Gateway running?", }; } return result; }), /** * Get live container stats (CPU%, RAM) for all running containers. */ stats: publicProcedure.query(async () => { const result = await getGatewayNodeStats(); if (!result) { return { stats: [] as import("./gateway-proxy").GatewayContainerStat[], count: 0, fetchedAt: new Date().toISOString(), error: "Gateway unavailable", }; } return result; }), }), /** * Tasks — управление задачами агентов */ tasks: router({ create: publicProcedure .input( z.object({ agentId: z.number(), conversationId: z.string().optional(), title: z.string().min(1), description: z.string().optional(), priority: z.enum(["low", "medium", "high", "critical"]).optional(), dependsOn: z.array(z.number()).optional(), metadata: z.record(z.string(), z.any()).optional(), }) ) .mutation(async ({ input }) => { const { createTask } = await import("./db"); return createTask({ agentId: input.agentId, conversationId: input.conversationId, title: input.title, description: input.description, priority: input.priority ?? "medium", dependsOn: input.dependsOn ?? [], metadata: input.metadata ?? {}, }); }), listByAgent: publicProcedure .input(z.object({ agentId: z.number() })) .query(async ({ input }) => { const { getAgentTasks } = await import("./db"); return getAgentTasks(input.agentId); }), listByConversation: publicProcedure .input(z.object({ conversationId: z.string() })) .query(async ({ input }) => { const { getConversationTasks } = await import("./db"); return getConversationTasks(input.conversationId); }), get: publicProcedure .input(z.object({ taskId: z.number() })) .query(async ({ input }) => { const { getTaskById } = await import("./db"); return getTaskById(input.taskId); }), update: publicProcedure .input( z.object({ taskId: z.number(), title: z.string().optional(), description: z.string().optional(), status: z .enum(["pending", "in_progress", "completed", "failed", "blocked"]) .optional(), priority: z.enum(["low", "medium", "high", "critical"]).optional(), result: z.string().optional(), errorMessage: z.string().optional(), startedAt: z.date().optional(), completedAt: z.date().optional(), metadata: z.record(z.string(), z.any()).optional(), }) ) .mutation(async ({ input }) => { const { updateTask } = await import("./db"); const { taskId, ...updates } = input; return updateTask(taskId, updates as any); }), delete: publicProcedure .input(z.object({ taskId: z.number() })) .mutation(async ({ input }) => { const { deleteTask } = await import("./db"); return deleteTask(input.taskId); }), getPending: publicProcedure .input(z.object({ agentId: z.number() })) .query(async ({ input }) => { const { getPendingAgentTasks } = await import("./db"); return getPendingAgentTasks(input.agentId); }), }), /** * Web Research Workflow — Browser Agent Integration */ research: router({ search: protectedProcedure .input( z.object({ query: z.string().min(1), maxResults: z.number().optional().default(5), includeScreenshots: z.boolean().optional().default(false), extractText: z.boolean().optional().default(false), conversationId: z.string().optional(), }) ) .mutation(async ({ input }) => { const { performWebResearch } = await import("./web-research"); const agentId = 1; const conversationId = input.conversationId || `conv-${Date.now()}`; return performWebResearch(agentId, conversationId, { query: input.query, maxResults: input.maxResults, includeScreenshots: input.includeScreenshots, extractText: input.extractText, }); }), compileReport: protectedProcedure .input( z.object({ results: z.array(z.any()), title: z.string(), }) ) .mutation(async ({ input }) => { const { compileResearchReport } = await import("./web-research"); return compileResearchReport(input.results, input.title); }), createTasks: protectedProcedure .input( z.object({ agentId: z.number(), conversationId: z.string(), queries: z.array(z.string()), }) ) .mutation(async ({ input }) => { const { createResearchTasks } = await import("./web-research"); return createResearchTasks( input.agentId, input.conversationId, input.queries ); }), }), }); export type AppRouter = typeof appRouter;