Skip to main content
Glama
agents.ts35.6 kB
/** * AI Agent API Routes * Provides REST endpoints for agent operations */ import { Router, Request, Response } from 'express'; import { logger } from '../logging/logger.js'; import { callLLM } from '../tools/llm/index.js'; import { getModelsByLayer, ModelConfig } from '../config/models.js'; import { ReActAgent, createReActAgent, AgentMemory, createAgentMemory, TaskDecomposer, createTaskDecomposer, toolSelector, createGroupChat, createCodeReviewGroup, createPlanningGroup, initializeAgentTables, AGENT_ROLES, type ReActTool, } from '../agents/index.js'; import { createDocumentSynthesizer } from '../agents/documentSynthesizer.js'; /** * Simple LLM wrapper for agents * Converts messages array to prompt/systemPrompt format for callLLM */ class AgentLLMClient { private model: ModelConfig; constructor() { // Get the best available L0 model const l0Models = getModelsByLayer('L0'); const l1Models = getModelsByLayer('L1'); this.model = l0Models[0] || l1Models[0] || { id: 'default', provider: 'openrouter', apiModelName: 'meta-llama/llama-3.3-70b-instruct:free', layer: 'L0', relativeCost: 0, capabilities: ['chat'], contextWindow: 8192, enabled: true, }; } async chat(options: { messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>; maxTokens?: number; temperature?: number; }): Promise<{ content: string; usage?: { totalTokens?: number } }> { // Convert messages to prompt/systemPrompt format let systemPrompt: string | undefined; const promptParts: string[] = []; for (const msg of options.messages) { if (msg.role === 'system') { systemPrompt = msg.content; } else if (msg.role === 'user') { promptParts.push(`User: ${msg.content}`); } else if (msg.role === 'assistant') { promptParts.push(`Assistant: ${msg.content}`); } } const prompt = promptParts.join('\n\n'); const response = await callLLM( { prompt, systemPrompt, maxTokens: options.maxTokens || 1000, temperature: options.temperature || 0.7, }, this.model ); return { content: response.content, usage: { totalTokens: response.inputTokens + response.outputTokens, }, }; } } /** * Create agent routes */ export function createAgentRoutes(): Router { const router = Router(); const llmClient = new AgentLLMClient(); // Initialize agent tables on startup initializeAgentTables().catch((error) => { logger.error('Failed to initialize agent tables', { error }); }); /** * Execute a ReAct agent task * POST /v1/agents/react */ router.post('/react', async (req: Request, res: Response) => { try { const { goal, tools = [], config = {} } = req.body; if (!goal) { res.status(400).json({ error: 'Goal is required' }); return; } // Build tools from request const agentTools: ReActTool[] = tools.map((t: { name: string; description: string; handler?: string }) => ({ name: t.name, description: t.description, parameters: {}, execute: async (args: Record<string, unknown>) => { // For now, return a placeholder - real tools would be injected return { message: `Tool ${t.name} called with args`, args }; }, })); // Add default tools if none provided if (agentTools.length === 0) { agentTools.push({ name: 'search', description: 'Search for information', parameters: { query: { type: 'string', description: 'Search query' } }, execute: async (args) => ({ results: [`Results for: ${args.query}`] }), }); agentTools.push({ name: 'analyze', description: 'Analyze data or text', parameters: { data: { type: 'string', description: 'Data to analyze' } }, execute: async (args) => ({ analysis: `Analysis of: ${args.data}` }), }); } const agent = createReActAgent(llmClient, agentTools, { maxIterations: config.maxIterations || 10, verbose: config.verbose || false, ...config, }); const result = await agent.execute(goal); res.json({ success: result.success, answer: result.answer, steps: result.steps, metadata: { iterations: result.iterations, toolsUsed: result.toolsUsed, totalTokens: result.totalTokens, duration: result.duration, }, }); } catch (error) { logger.error('ReAct agent error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Agent execution failed', }); } }); /** * Decompose a goal into tasks * POST /v1/agents/decompose */ router.post('/decompose', async (req: Request, res: Response) => { try { const { goal, context } = req.body; if (!goal) { res.status(400).json({ error: 'Goal is required' }); return; } const decomposer = createTaskDecomposer(llmClient); const tasks = await decomposer.decompose(goal, context); res.json({ goal, tasks: tasks.map((t) => ({ id: t.id, title: t.title, description: t.description, status: t.status, priority: t.priority, dependencies: t.dependencies, complexity: t.metadata.estimatedComplexity, })), progress: decomposer.getProgress(), }); } catch (error) { logger.error('Task decomposition error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Decomposition failed', }); } }); /** * Store a memory * POST /v1/agents/memory */ router.post('/memory', async (req: Request, res: Response) => { try { const { conversationId, content, type = 'episodic', importance, tags } = req.body; if (!conversationId || !content) { res.status(400).json({ error: 'conversationId and content are required' }); return; } const memory = createAgentMemory(conversationId); const entry = await memory.store(content, type, { importance, tags }); res.json({ id: entry.id, type: entry.type, stored: true, importance: entry.metadata.importance, }); } catch (error) { logger.error('Memory store error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to store memory', }); } }); /** * Retrieve memories * GET /v1/agents/memory/:conversationId */ router.get('/memory/:conversationId', async (req: Request, res: Response) => { try { const { conversationId } = req.params; const { query, type, limit } = req.query; const memory = createAgentMemory(conversationId); if (query) { const results = await memory.retrieve(query as string, { type: type as 'episodic' | 'semantic' | 'procedural', limit: limit ? parseInt(limit as string) : undefined, }); res.json({ memories: results }); } else { const recent = await memory.getRecent(limit ? parseInt(limit as string) : 10); res.json({ memories: recent }); } } catch (error) { logger.error('Memory retrieve error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to retrieve memories', }); } }); /** * Get tool recommendations * POST /v1/agents/tools/recommend */ router.post('/tools/recommend', async (req: Request, res: Response) => { try { const { taskContext, availableTools } = req.body; if (!taskContext || !availableTools) { res.status(400).json({ error: 'taskContext and availableTools are required' }); return; } const recommendations = await toolSelector.recommend(taskContext, availableTools); res.json({ recommendations }); } catch (error) { logger.error('Tool recommendation error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to get recommendations', }); } }); /** * Record tool execution result * POST /v1/agents/tools/record */ router.post('/tools/record', async (req: Request, res: Response) => { try { const { toolName, success, duration, taskContext, errorType } = req.body; if (!toolName || success === undefined || !duration) { res.status(400).json({ error: 'toolName, success, and duration are required', }); return; } await toolSelector.recordExecution({ toolName, success, duration, taskContext: taskContext || '', errorType, }); res.json({ recorded: true }); } catch (error) { logger.error('Tool record error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to record execution', }); } }); /** * Get tool statistics * GET /v1/agents/tools/stats */ router.get('/tools/stats', async (_req: Request, res: Response) => { try { const stats = await toolSelector.getStatistics(); res.json(stats); } catch (error) { logger.error('Tool stats error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to get statistics', }); } }); /** * Start a group chat * POST /v1/agents/group-chat */ router.post('/group-chat', async (req: Request, res: Response) => { try { const { topic, roles, type = 'custom', config = {} } = req.body; if (!topic) { res.status(400).json({ error: 'Topic is required' }); return; } let groupChat; if (type === 'code-review') { groupChat = createCodeReviewGroup(llmClient, topic, config); } else if (type === 'planning') { groupChat = createPlanningGroup(llmClient, topic, config); } else { if (!roles || roles.length === 0) { res.status(400).json({ error: 'Roles are required for custom group chat' }); return; } groupChat = createGroupChat(llmClient, topic, roles, config); } const conversation = await groupChat.start(); res.json({ id: conversation.id, topic: conversation.topic, status: conversation.status, participants: conversation.participants.map((p) => ({ id: p.id, name: p.name, expertise: p.expertise, })), messages: conversation.messages, conclusion: conversation.conclusion, metadata: conversation.metadata, }); } catch (error) { logger.error('Group chat error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Group chat failed', }); } }); /** * Get available agent roles * GET /v1/agents/roles */ router.get('/roles', (_req: Request, res: Response) => { res.json({ roles: Object.entries(AGENT_ROLES).map(([id, role]) => ({ id, name: role.name, description: role.description, expertise: role.expertise, tools: role.tools, })), }); }); /** * Synthesize multiple documents into a single developer guide (Markdown) * POST /v1/agents/synthesize-docs */ router.post('/synthesize-docs', async (req: Request, res: Response) => { try { const { documents, title, includeTOC = true, writeToFile = false } = req.body; if (!documents || !Array.isArray(documents) || documents.length === 0) { res.status(400).json({ error: 'documents array is required' }); return; } const synthesizer = createDocumentSynthesizer(llmClient as any); const md = await synthesizer.synthesize(documents, { title, includeTOC }); if (writeToFile) { try { const fs = await import('fs'); await fs.promises.writeFile('dev-guide.md', md, 'utf8'); } catch (err) { logger.warn('Failed to write dev-guide.md', { err }); } } res.json({ success: true, content: md }); } catch (error) { logger.error('Document synthesis error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Synthesis failed' }); } }); /** * Combined agent endpoint for complex tasks * POST /v1/agents/execute * This combines decomposition, tool selection, and ReAct execution */ router.post('/execute', async (req: Request, res: Response) => { try { const { goal, context, useDecomposition = true, useGroupChat = false } = req.body; if (!goal) { res.status(400).json({ error: 'Goal is required' }); return; } const startTime = Date.now(); const results: { decomposition?: unknown; groupChat?: unknown; execution?: unknown; } = {}; // Step 1: Decompose if enabled if (useDecomposition) { const decomposer = createTaskDecomposer(llmClient); const tasks = await decomposer.decompose(goal, context); results.decomposition = { tasks: tasks.map((t) => ({ id: t.id, title: t.title, complexity: t.metadata.estimatedComplexity, })), }; } // Step 2: Group chat for planning if enabled if (useGroupChat) { const planningGroup = createPlanningGroup(llmClient, goal, { maxTurns: 5 }); const conversation = await planningGroup.start(); results.groupChat = { conclusion: conversation.conclusion, turns: conversation.metadata.turnCount, }; } // Step 3: Execute with ReAct agent const defaultTools: ReActTool[] = [ { name: 'think', description: 'Think through a problem step by step', parameters: { problem: { type: 'string', description: 'Problem to think about' } }, execute: async (args) => ({ thought: `Thinking about: ${args.problem}` }), }, { name: 'conclude', description: 'Draw a conclusion from analysis', parameters: { analysis: { type: 'string', description: 'Analysis to conclude from' } }, execute: async (args) => ({ conclusion: `Conclusion from: ${args.analysis}` }), }, ]; const agent = createReActAgent(llmClient, defaultTools, { maxIterations: 5 }); const execution = await agent.execute(goal); results.execution = { success: execution.success, answer: execution.answer, iterations: execution.iterations, }; const duration = Date.now() - startTime; res.json({ goal, success: execution.success, answer: execution.answer, results, duration, }); } catch (error) { logger.error('Agent execute error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Execution failed', }); } }); // ============================================ // NEW SPECIALIZED AGENTS // ============================================ /** * Web Research endpoint * POST /v1/agents/research */ router.post('/research', async (req: Request, res: Response) => { try { const { topic, context, sources = [], focusAreas, maxSources = 10 } = req.body; if (!topic) { res.status(400).json({ error: 'Topic is required' }); return; } const { createWebResearcher } = await import('../agents/webResearcher.js'); const researcher = createWebResearcher(llmClient); const result = await researcher.research( { topic, context, focusAreas, maxSources }, sources ); res.json(result); } catch (error) { logger.error('Web research error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Research failed', }); } }); /** * Fact check endpoint * POST /v1/agents/fact-check */ router.post('/fact-check', async (req: Request, res: Response) => { try { const { claim, sources = [] } = req.body; if (!claim) { res.status(400).json({ error: 'Claim is required' }); return; } const { createWebResearcher } = await import('../agents/webResearcher.js'); const researcher = createWebResearcher(llmClient); const result = await researcher.factCheck(claim, sources); res.json(result); } catch (error) { logger.error('Fact check error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Fact check failed', }); } }); /** * Code analysis endpoint * POST /v1/agents/analyze-code */ router.post('/analyze-code', async (req: Request, res: Response) => { try { const { file, files } = req.body; if (!file && !files) { res.status(400).json({ error: 'File or files array is required' }); return; } const { createCodeAnalyzer } = await import('../agents/codeAnalyzer.js'); const analyzer = createCodeAnalyzer(llmClient); if (files && Array.isArray(files)) { const result = await analyzer.analyzeProject(files); res.json(result); } else { const result = await analyzer.analyze(file); res.json(result); } } catch (error) { logger.error('Code analysis error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Code analysis failed', }); } }); /** * Code security scan endpoint * POST /v1/agents/security-scan */ router.post('/security-scan', async (req: Request, res: Response) => { try { const { file } = req.body; if (!file || !file.path || !file.content) { res.status(400).json({ error: 'File with path and content is required' }); return; } const { createCodeAnalyzer } = await import('../agents/codeAnalyzer.js'); const analyzer = createCodeAnalyzer(llmClient); const result = await analyzer.securityScan(file); res.json(result); } catch (error) { logger.error('Security scan error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Security scan failed', }); } }); /** * Generate documentation endpoint * POST /v1/agents/generate-docs */ router.post('/generate-docs', async (req: Request, res: Response) => { try { const { file, format = 'tsdoc' } = req.body; if (!file || !file.path || !file.content) { res.status(400).json({ error: 'File with path and content is required' }); return; } const { createCodeAnalyzer } = await import('../agents/codeAnalyzer.js'); const analyzer = createCodeAnalyzer(llmClient); const result = await analyzer.generateDocumentation(file, format); res.json(result); } catch (error) { logger.error('Generate docs error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Documentation generation failed', }); } }); /** * Data extraction endpoint * POST /v1/agents/extract-data */ router.post('/extract-data', async (req: Request, res: Response) => { try { const { content, schema, type = 'schema' } = req.body; if (!content) { res.status(400).json({ error: 'Content is required' }); return; } const { createDataExtractor } = await import('../agents/dataExtractor.js'); const extractor = createDataExtractor(llmClient); let result; switch (type) { case 'entities': result = await extractor.extractEntities(content, req.body.entityTypes); break; case 'table': result = await extractor.extractTable(content, req.body.columns); break; case 'keyvalues': result = await extractor.extractKeyValues(content); break; case 'schema': default: if (!schema) { res.status(400).json({ error: 'Schema is required for schema extraction' }); return; } result = await extractor.extractWithSchema(content, schema); break; } res.json(result); } catch (error) { logger.error('Data extraction error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Data extraction failed', }); } }); /** * Infer schema from example data * POST /v1/agents/infer-schema */ router.post('/infer-schema', async (req: Request, res: Response) => { try { const { exampleData, schemaName } = req.body; if (!exampleData) { res.status(400).json({ error: 'Example data is required' }); return; } const { createDataExtractor } = await import('../agents/dataExtractor.js'); const extractor = createDataExtractor(llmClient); const schema = await extractor.inferSchema(exampleData, schemaName); res.json(schema); } catch (error) { logger.error('Schema inference error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Schema inference failed', }); } }); // ============================================ // WORKFLOW ORCHESTRATOR & N8N INTEGRATION // ============================================ // Lazy initialization for workflow orchestrator let _workflowOrchestrator: Awaited<ReturnType<typeof import('../agents/workflowOrchestrator.js').createWorkflowOrchestrator>> | null = null; const getWorkflowOrchestrator = async () => { if (!_workflowOrchestrator) { const { createWorkflowOrchestrator } = await import('../agents/workflowOrchestrator.js'); _workflowOrchestrator = createWorkflowOrchestrator(llmClient); } return _workflowOrchestrator; }; /** * Register a workflow * POST /v1/agents/workflows */ router.post('/workflows', async (req: Request, res: Response) => { try { const workflow = req.body; if (!workflow.id || !workflow.name || !workflow.steps) { res.status(400).json({ error: 'Workflow id, name, and steps are required' }); return; } const workflowOrchestrator = await getWorkflowOrchestrator(); workflowOrchestrator.registerWorkflow(workflow); res.json({ success: true, message: `Workflow ${workflow.id} registered`, workflow }); } catch (error) { logger.error('Register workflow error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to register workflow', }); } }); /** * List all workflows * GET /v1/agents/workflows */ router.get('/workflows', async (_req: Request, res: Response) => { try { const workflowOrchestrator = await getWorkflowOrchestrator(); const workflows = workflowOrchestrator.listWorkflows(); res.json({ workflows }); } catch (error) { logger.error('List workflows error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to list workflows', }); } }); /** * Get a specific workflow * GET /v1/agents/workflows/:workflowId */ router.get('/workflows/:workflowId', async (req: Request, res: Response) => { try { const workflowOrchestrator = await getWorkflowOrchestrator(); const workflow = workflowOrchestrator.getWorkflow(req.params.workflowId); if (!workflow) { res.status(404).json({ error: 'Workflow not found' }); return; } res.json(workflow); } catch (error) { logger.error('Get workflow error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to get workflow', }); } }); /** * Execute a workflow * POST /v1/agents/workflows/:workflowId/execute */ router.post('/workflows/:workflowId/execute', async (req: Request, res: Response) => { try { const { workflowId } = req.params; const input = req.body; const workflowOrchestrator = await getWorkflowOrchestrator(); const execution = await workflowOrchestrator.executeWorkflow(workflowId, input); res.json(execution); } catch (error) { logger.error('Execute workflow error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to execute workflow', }); } }); /** * Get workflow execution status * GET /v1/agents/workflows/executions/:executionId */ router.get('/workflows/executions/:executionId', async (req: Request, res: Response) => { try { const workflowOrchestrator = await getWorkflowOrchestrator(); const execution = workflowOrchestrator.getExecution(req.params.executionId); if (!execution) { res.status(404).json({ error: 'Execution not found' }); return; } res.json(execution); } catch (error) { logger.error('Get execution error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to get execution', }); } }); /** * Generate a workflow from natural language * POST /v1/agents/workflows/generate */ router.post('/workflows/generate', async (req: Request, res: Response) => { try { const { description } = req.body; if (!description) { res.status(400).json({ error: 'Description is required' }); return; } const workflowOrchestrator = await getWorkflowOrchestrator(); const workflow = await workflowOrchestrator.generateWorkflow(description); res.json(workflow); } catch (error) { logger.error('Generate workflow error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to generate workflow', }); } }); /** * Register n8n webhook * POST /v1/agents/webhooks */ router.post('/webhooks', async (req: Request, res: Response) => { try { const { name, url, method = 'POST', headers, authentication } = req.body; if (!name || !url) { res.status(400).json({ error: 'Webhook name and url are required' }); return; } const workflowOrchestrator = await getWorkflowOrchestrator(); workflowOrchestrator.registerWebhook(name, { url, method, headers, authentication, }); res.json({ success: true, message: `Webhook ${name} registered` }); } catch (error) { logger.error('Register webhook error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to register webhook', }); } }); /** * Trigger n8n webhook * POST /v1/agents/webhooks/:webhookName/trigger */ router.post('/webhooks/:webhookName/trigger', async (req: Request, res: Response) => { try { const { webhookName } = req.params; const data = req.body; const workflowOrchestrator = await getWorkflowOrchestrator(); const result = await workflowOrchestrator.callN8nWebhook(webhookName, data); res.json({ success: true, result }); } catch (error) { logger.error('Trigger webhook error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to trigger webhook', }); } }); /** * n8n-compatible webhook receiver * POST /v1/agents/n8n/webhook * This endpoint can be called by n8n to trigger agent actions */ router.post('/n8n/webhook', async (req: Request, res: Response) => { try { const { action, payload } = req.body; logger.info('n8n webhook received', { action }); let result: unknown; switch (action) { case 'research': const { createWebResearcher } = await import('../agents/webResearcher.js'); const researcher = createWebResearcher(llmClient); result = await researcher.research(payload.query, payload.sources || []); break; case 'analyze-code': const { createCodeAnalyzer } = await import('../agents/codeAnalyzer.js'); const analyzer = createCodeAnalyzer(llmClient); result = await analyzer.analyze(payload.file); break; case 'extract-data': const { createDataExtractor } = await import('../agents/dataExtractor.js'); const extractor = createDataExtractor(llmClient); result = await extractor.extractWithSchema(payload.content, payload.schema); break; case 'synthesize-docs': const synthesizer = createDocumentSynthesizer(llmClient); result = await synthesizer.synthesize(payload.sources, payload.options); break; case 'execute-workflow': const wfOrchestrator = await getWorkflowOrchestrator(); result = await wfOrchestrator.executeWorkflow(payload.workflowId, payload.input); break; case 'decompose-task': const decomposer = createTaskDecomposer(llmClient); result = await decomposer.decompose(payload.goal, payload.context); break; case 'react': const defaultTools: ReActTool[] = [ { name: 'analyze', description: 'Analyze information', parameters: {}, execute: async (args) => ({ analysis: JSON.stringify(args) }), }, ]; const agent = createReActAgent(llmClient, defaultTools, { maxIterations: 5 }); result = await agent.execute(payload.goal); break; default: res.status(400).json({ error: `Unknown action: ${action}` }); return; } res.json({ success: true, action, result, timestamp: Date.now(), }); } catch (error) { logger.error('n8n webhook error', { error }); res.status(500).json({ error: error instanceof Error ? error.message : 'n8n webhook failed', }); } }); return router; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server