UNPKG

@hanzo/dev

Version:

Hanzo Dev - Meta AI development CLI that manages and runs all LLMs and CLI tools

552 lines (480 loc) • 16.5 kB
import { EventEmitter } from 'events'; import chalk from 'chalk'; import { FunctionCallingSystem, FunctionCall } from './function-calling'; import { MCPClient, MCPSession } from './mcp-client'; export interface LLMProvider { name: string; type: 'openai' | 'anthropic' | 'local' | 'hanzo-app'; apiKey?: string; baseUrl?: string; model: string; supportsTools: boolean; supportsStreaming: boolean; } export interface AgentLoopConfig { provider: LLMProvider; maxIterations: number; enableMCP: boolean; enableBrowser: boolean; enableSwarm: boolean; streamOutput: boolean; confirmActions: boolean; } export interface AgentMessage { role: 'system' | 'user' | 'assistant' | 'tool'; content: string; toolCalls?: FunctionCall[]; toolResults?: any[]; } export class ConfigurableAgentLoop extends EventEmitter { private config: AgentLoopConfig; private functionCalling: FunctionCallingSystem; private mcpClient: MCPClient; private messages: AgentMessage[] = []; private iterations: number = 0; constructor(config: AgentLoopConfig) { super(); this.config = config; this.functionCalling = new FunctionCallingSystem(); this.mcpClient = new MCPClient(); } // Get available LLM providers static getAvailableProviders(): LLMProvider[] { const providers: LLMProvider[] = []; // Check for API keys if (process.env.ANTHROPIC_API_KEY) { providers.push({ name: 'Claude (Anthropic)', type: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-3-opus-20240229', supportsTools: true, supportsStreaming: true }); } if (process.env.OPENAI_API_KEY) { providers.push({ name: 'GPT-4 (OpenAI)', type: 'openai', apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4-turbo-preview', supportsTools: true, supportsStreaming: true }); } // Check for local Hanzo App if (process.env.HANZO_APP_URL || this.isHanzoAppRunning()) { providers.push({ name: 'Hanzo Local AI', type: 'hanzo-app', baseUrl: process.env.HANZO_APP_URL || 'http://localhost:8080', model: 'hanzo-zen', supportsTools: true, supportsStreaming: false }); } // Check for other local models if (process.env.LOCAL_LLM_URL) { providers.push({ name: 'Local LLM', type: 'local', baseUrl: process.env.LOCAL_LLM_URL, model: process.env.LOCAL_LLM_MODEL || 'llama2', supportsTools: false, supportsStreaming: true }); } return providers; } private static isHanzoAppRunning(): boolean { // Check if Hanzo App is running locally try { const { execSync } = require('child_process'); execSync('curl -s http://localhost:8080/health', { stdio: 'ignore' }); return true; } catch { return false; } } // Initialize tools and connections async initialize(): Promise<void> { console.log(chalk.cyan('\nšŸ”§ Initializing agent loop...\n')); // Initialize MCP tools if enabled if (this.config.enableMCP) { await this.initializeMCPTools(); } // Initialize browser connection if enabled if (this.config.enableBrowser) { await this.initializeBrowserTools(); } // Initialize swarm if enabled if (this.config.enableSwarm) { await this.initializeSwarmTools(); } console.log(chalk.green('āœ“ Agent loop initialized\n')); } private async initializeMCPTools(): Promise<void> { console.log(chalk.gray('Loading MCP tools...')); // Load configured MCP servers from config file const mcpConfig = await this.loadMCPConfig(); for (const server of mcpConfig.servers) { try { console.log(chalk.gray(` Connecting to ${server.name}...`)); const session = await this.mcpClient.connect(server); await this.functionCalling.registerMCPServer(server.name, session); console.log(chalk.green(` āœ“ Connected to ${server.name} (${session.tools.length} tools)`)); } catch (error) { console.log(chalk.yellow(` ⚠ Failed to connect to ${server.name}`)); } } } private async initializeBrowserTools(): Promise<void> { console.log(chalk.gray('Connecting to browser...')); // Check for Hanzo Browser Extension if (await this.checkBrowserExtension()) { console.log(chalk.green(' āœ“ Connected to Hanzo Browser Extension')); this.registerBrowserTools('extension'); } // Check for Hanzo Browser else if (await this.checkHanzoBrowser()) { console.log(chalk.green(' āœ“ Connected to Hanzo Browser')); this.registerBrowserTools('browser'); } else { console.log(chalk.yellow(' ⚠ No browser connection available')); } } private async checkBrowserExtension(): Promise<boolean> { // Check if browser extension is available via WebSocket try { const ws = new (require('ws'))('ws://localhost:9222/hanzo-extension'); return new Promise((resolve) => { ws.on('open', () => { ws.close(); resolve(true); }); ws.on('error', () => resolve(false)); setTimeout(() => { ws.close(); resolve(false); }, 1000); }); } catch { return false; } } private async checkHanzoBrowser(): Promise<boolean> { // Check if Hanzo Browser is running try { const response = await fetch('http://localhost:9223/status'); return response.ok; } catch { return false; } } private registerBrowserTools(type: 'extension' | 'browser'): void { // Register browser automation tools const browserTools = [ { name: 'browser_navigate', description: 'Navigate to a URL in the browser', parameters: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' } }, required: ['url'] }, handler: async (args: any) => this.browserNavigate(args.url) }, { name: 'browser_click', description: 'Click on an element in the browser', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector or element ID' } }, required: ['selector'] }, handler: async (args: any) => this.browserClick(args.selector) }, { name: 'browser_screenshot', description: 'Take a screenshot of the current page', parameters: { type: 'object', properties: { fullPage: { type: 'boolean', description: 'Capture full page' } } }, handler: async (args: any) => this.browserScreenshot(args.fullPage) }, { name: 'browser_fill', description: 'Fill a form field in the browser', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector' }, value: { type: 'string', description: 'Value to fill' } }, required: ['selector', 'value'] }, handler: async (args: any) => this.browserFill(args.selector, args.value) } ]; browserTools.forEach(tool => this.functionCalling.registerTool(tool)); } private async initializeSwarmTools(): Promise<void> { console.log(chalk.gray('Initializing swarm tools...')); // Register swarm coordination tools this.functionCalling.registerTool({ name: 'spawn_agent', description: 'Spawn a new agent for a specific task', parameters: { type: 'object', properties: { task: { type: 'string', description: 'Task for the agent' }, agentType: { type: 'string', enum: ['claude-code', 'aider', 'openhands'] } }, required: ['task'] }, handler: async (args: any) => this.spawnAgent(args.task, args.agentType) }); this.functionCalling.registerTool({ name: 'delegate_to_swarm', description: 'Delegate multiple tasks to agent swarm', parameters: { type: 'object', properties: { tasks: { type: 'array', items: { type: 'string' }, description: 'List of tasks to delegate' } }, required: ['tasks'] }, handler: async (args: any) => this.delegateToSwarm(args.tasks) }); } // Execute agent loop async execute(initialPrompt: string): Promise<void> { console.log(chalk.cyan(`\nšŸ¤– Starting agent loop with ${this.config.provider.name}\n`)); // Add system message this.messages.push({ role: 'system', content: this.getSystemPrompt() }); // Add user message this.messages.push({ role: 'user', content: initialPrompt }); // Main agent loop while (this.iterations < this.config.maxIterations) { this.iterations++; console.log(chalk.blue(`\nā–¶ Iteration ${this.iterations}`)); try { // Get LLM response const response = await this.callLLM(); // Process response if (response.toolCalls && response.toolCalls.length > 0) { // Execute tool calls const results = await this.executeToolCalls(response.toolCalls); // Add tool results to messages this.messages.push({ role: 'tool', content: JSON.stringify(results), toolResults: results }); } else { // No more tool calls, task complete console.log(chalk.green('\nāœ… Task completed')); break; } } catch (error) { console.error(chalk.red(`Error in iteration ${this.iterations}: ${error}`)); this.emit('error', error); break; } } if (this.iterations >= this.config.maxIterations) { console.log(chalk.yellow('\n⚠ Maximum iterations reached')); } } private getSystemPrompt(): string { const tools = this.functionCalling.getAvailableTools(); return `You are an AI assistant with access to various tools. You can: - Edit files using view_file, create_file, str_replace - Run commands using run_command - Search files using search_files - List directories using list_directory ${this.config.enableBrowser ? '- Control the browser using browser_* tools' : ''} ${this.config.enableSwarm ? '- Spawn agents and delegate tasks using swarm tools' : ''} ${this.config.enableMCP ? '- Use MCP tools for extended functionality' : ''} Available tools: ${tools.map(t => t.name).join(', ')} Always use tools to accomplish tasks. Think step by step.`; } private async callLLM(): Promise<AgentMessage> { const { provider } = this.config; switch (provider.type) { case 'anthropic': return this.callAnthropic(); case 'openai': return this.callOpenAI(); case 'hanzo-app': return this.callHanzoApp(); case 'local': return this.callLocalLLM(); default: throw new Error(`Unknown provider type: ${provider.type}`); } } private async callAnthropic(): Promise<AgentMessage> { // Implementation for Anthropic Claude console.log(chalk.gray('Calling Claude...')); // This is a simplified version - in production you'd use the actual API const tools = this.functionCalling.getAllToolSchemas(); // Simulate API call const response = { role: 'assistant' as const, content: 'I will help you with this task.', toolCalls: [ { id: 'call_1', name: 'view_file', arguments: { path: 'package.json' } } ] }; this.messages.push(response); return response; } private async callOpenAI(): Promise<AgentMessage> { // Implementation for OpenAI console.log(chalk.gray('Calling GPT-4...')); // Similar to Anthropic but with OpenAI API format const response = { role: 'assistant' as const, content: 'I will analyze and complete this task.', toolCalls: [] }; this.messages.push(response); return response; } private async callHanzoApp(): Promise<AgentMessage> { // Implementation for local Hanzo App console.log(chalk.gray('Calling Hanzo Local AI...')); const response = await fetch(`${this.config.provider.baseUrl}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: this.messages, tools: this.functionCalling.getAllToolSchemas() }) }); const data = await response.json(); this.messages.push(data); return data; } private async callLocalLLM(): Promise<AgentMessage> { // Implementation for generic local LLM console.log(chalk.gray('Calling Local LLM...')); // Local LLMs might not support tools, so we use a different approach const response = { role: 'assistant' as const, content: 'Processing your request...', toolCalls: [] }; this.messages.push(response); return response; } private async executeToolCalls(calls: FunctionCall[]): Promise<any[]> { if (this.config.confirmActions) { console.log(chalk.yellow('\n⚠ Tool calls requested:')); calls.forEach(call => { console.log(` - ${call.name}(${JSON.stringify(call.arguments)})`); }); const { confirm } = await require('inquirer').prompt([{ type: 'confirm', name: 'confirm', message: 'Execute these actions?', default: true }]); if (!confirm) { throw new Error('User cancelled tool execution'); } } return this.functionCalling.callFunctions(calls); } // Browser tool implementations private async browserNavigate(url: string): Promise<any> { console.log(chalk.gray(`Navigating to ${url}...`)); // Implementation would connect to browser return { success: true, url }; } private async browserClick(selector: string): Promise<any> { console.log(chalk.gray(`Clicking ${selector}...`)); return { success: true, selector }; } private async browserScreenshot(fullPage: boolean = false): Promise<any> { console.log(chalk.gray(`Taking screenshot (fullPage: ${fullPage})...`)); return { success: true, screenshot: 'base64_image_data' }; } private async browserFill(selector: string, value: string): Promise<any> { console.log(chalk.gray(`Filling ${selector} with "${value}"...`)); return { success: true, selector, value }; } // Swarm tool implementations private async spawnAgent(task: string, agentType?: string): Promise<any> { console.log(chalk.gray(`Spawning ${agentType || 'default'} agent for: ${task}`)); return { success: true, agentId: `agent-${Date.now()}`, task }; } private async delegateToSwarm(tasks: string[]): Promise<any> { console.log(chalk.gray(`Delegating ${tasks.length} tasks to swarm...`)); return { success: true, tasks, status: 'delegated' }; } // Load MCP configuration private async loadMCPConfig(): Promise<{ servers: any[] }> { const fs = require('fs'); const path = require('path'); const os = require('os'); // Check multiple locations for MCP config const configPaths = [ path.join(process.cwd(), '.mcp.json'), path.join(os.homedir(), '.config', 'hanzo-dev', 'mcp.json'), path.join(os.homedir(), '.hanzo', 'mcp.json') ]; for (const configPath of configPaths) { if (fs.existsSync(configPath)) { const content = fs.readFileSync(configPath, 'utf-8'); return JSON.parse(content); } } // Default MCP servers return { servers: [ { name: 'filesystem', command: 'npx', args: ['@modelcontextprotocol/server-filesystem'], env: { MCP_ALLOWED_PATHS: process.cwd() } }, { name: 'git', command: 'npx', args: ['@modelcontextprotocol/server-git'] } ] }; } // Get current configuration getConfig(): AgentLoopConfig { return this.config; } // Update configuration updateConfig(updates: Partial<AgentLoopConfig>): void { this.config = { ...this.config, ...updates }; } }