/** * Tool Emulation Layer for Models Without Native Function Calling * * Implements two strategies: * 1. ReAct Pattern - Structured reasoning with tool use * 2. Prompt-Based - Direct JSON tool invocation * * Automatically selected based on model capabilities. */ /** * ReAct Pattern Implementation * Best for: Models with 32k+ context, complex multi-step tasks */ export class ReActEmulator { tools; constructor(tools) { this.tools = tools; } /** * Build ReAct prompt with tool catalog */ buildPrompt(userMessage, previousSteps = '') { const toolCatalog = this.tools.map(tool => { const params = tool.input_schema?.properties || {}; const required = tool.input_schema?.required || []; const paramDocs = Object.entries(params).map(([name, schema]) => { const req = required.includes(name) ? '(required)' : '(optional)'; const type = schema.type || 'any'; return ` - ${name} ${req}: ${type} - ${schema.description || ''}`; }).join('\n'); return `• ${tool.name}: ${tool.description || 'No description'} ${paramDocs}`; }).join('\n\n'); return `You are solving a task using available tools. Think step-by-step using this format: Thought: [Your reasoning about what to do next] Action: [tool_name] Action Input: [JSON object with tool parameters] Observation: [Tool result will be inserted here by the system] ... (repeat Thought/Action/Observation as needed) Final Answer: [Your complete answer to the user's question] Available Tools: ${toolCatalog} IMPORTANT: - Action Input must be valid JSON matching the tool's schema - Only use tools from the list above - When you have enough information, provide a Final Answer - If a tool fails, think about alternative approaches ${previousSteps} User Question: ${userMessage} Begin!`; } /** * Parse ReAct response and extract tool calls */ parseResponse(response) { // Extract components ([\s\S] instead of /s flag for ES5 compatibility) const thoughtMatch = response.match(/Thought:\s*([\s\S]+?)(?=\n(?:Action:|Final Answer:|$))/); const actionMatch = response.match(/Action:\s*(\w+)/); const inputMatch = response.match(/Action Input:\s*(\{[\s\S]*?\})/); const finalMatch = response.match(/Final Answer:\s*([\s\S]+?)$/); if (finalMatch) { return { finalAnswer: finalMatch[1].trim(), thought: thoughtMatch?.[1].trim() }; } if (actionMatch && inputMatch) { try { const args = JSON.parse(inputMatch[1].trim()); return { toolCall: { name: actionMatch[1], arguments: args, id: `react_${Date.now()}` }, thought: thoughtMatch?.[1].trim() }; } catch (e) { console.error('Failed to parse Action Input JSON:', e); return { thought: thoughtMatch?.[1].trim() }; } } return { thought: thoughtMatch?.[1].trim() }; } /** * Build prompt with observation after tool execution */ appendObservation(previousPrompt, observation) { return `${previousPrompt}\nObservation: ${observation}\n`; } } /** * Prompt-Based Tool Emulation * Best for: Simple tasks, models with limited context */ export class PromptEmulator { tools; constructor(tools) { this.tools = tools; } /** * Build simple prompt for tool invocation */ buildPrompt(userMessage) { const toolCatalog = this.tools.map(tool => { const params = tool.input_schema?.properties || {}; const paramList = Object.keys(params).join(', '); return `${tool.name}(${paramList}): ${tool.description || 'No description'}`; }).join('\n'); return `You have access to these tools: ${toolCatalog} To use a tool, respond with ONLY this JSON format (no other text): { "tool": "tool_name", "arguments": { "param1": "value1", "param2": "value2" } } If you don't need a tool, respond with your answer normally. User: ${userMessage} Response:`; } /** * Parse response - either tool call JSON or regular text */ parseResponse(response) { // Try to extract JSON from response const jsonMatch = response.match(/\{[\s\S]*\}/); if (!jsonMatch) { return { textResponse: response.trim() }; } try { const parsed = JSON.parse(jsonMatch[0]); if (parsed.tool && parsed.arguments) { return { toolCall: { name: parsed.tool, arguments: parsed.arguments, id: `prompt_${Date.now()}` } }; } } catch (e) { // Not valid JSON, treat as text response } return { textResponse: response.trim() }; } } /** * Unified Tool Emulation Interface */ export class ToolEmulator { tools; strategy; reactEmulator; promptEmulator; constructor(tools, strategy) { this.tools = tools; this.strategy = strategy; this.reactEmulator = new ReActEmulator(tools); this.promptEmulator = new PromptEmulator(tools); } /** * Build prompt based on selected strategy */ buildPrompt(userMessage, context) { if (this.strategy === 'react') { return this.reactEmulator.buildPrompt(userMessage, context?.previousSteps); } else { return this.promptEmulator.buildPrompt(userMessage); } } /** * Parse model response and extract tool calls */ parseResponse(response) { if (this.strategy === 'react') { return this.reactEmulator.parseResponse(response); } else { return this.promptEmulator.parseResponse(response); } } /** * Append observation (ReAct only) */ appendObservation(prompt, observation) { if (this.strategy === 'react') { return this.reactEmulator.appendObservation(prompt, observation); } return prompt; } /** * Validate tool call against schema */ validateToolCall(toolCall) { const tool = this.tools.find(t => t.name === toolCall.name); if (!tool) { return { valid: false, errors: [`Tool '${toolCall.name}' not found. Available: ${this.tools.map(t => t.name).join(', ')}`] }; } const errors = []; const schema = tool.input_schema; if (!schema) { return { valid: true }; // No schema to validate against } // Check required parameters const required = schema.required || []; for (const param of required) { if (!(param in toolCall.arguments)) { errors.push(`Missing required parameter: ${param}`); } } // Type checking (basic) const properties = schema.properties || {}; for (const [key, value] of Object.entries(toolCall.arguments)) { if (properties[key]) { const expectedType = properties[key].type; const actualType = typeof value; if (expectedType === 'string' && actualType !== 'string') { errors.push(`Parameter '${key}' must be string, got ${actualType}`); } else if (expectedType === 'number' && actualType !== 'number') { errors.push(`Parameter '${key}' must be number, got ${actualType}`); } else if (expectedType === 'boolean' && actualType !== 'boolean') { errors.push(`Parameter '${key}' must be boolean, got ${actualType}`); } } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Get confidence score for emulation result * Based on: JSON validity, schema compliance, reasoning quality */ getConfidence(parsed) { let confidence = 0.5; // Base confidence if (parsed.toolCall) { const validation = this.validateToolCall(parsed.toolCall); if (validation.valid) { confidence += 0.3; // Valid tool call } else { confidence -= 0.2; // Invalid tool call } } if (parsed.thought && parsed.thought.length > 20) { confidence += 0.1; // Good reasoning } if (parsed.finalAnswer && parsed.finalAnswer.length > 10) { confidence += 0.1; // Complete answer } return Math.max(0, Math.min(1, confidence)); } } /** * Execute tool emulation loop */ export async function executeEmulation(emulator, userMessage, modelCall, toolExecutor, options = {}) { const maxIterations = options.maxIterations || 5; const verbose = options.verbose || false; let prompt = emulator.buildPrompt(userMessage); const toolCalls = []; let finalAnswer; let lastReasoning; if (verbose) { console.log('\nšŸ”§ Starting tool emulation...\n'); } for (let i = 0; i < maxIterations; i++) { if (verbose) { console.log(`\n━━━ Iteration ${i + 1}/${maxIterations} ━━━`); } // Call model const response = await modelCall(prompt); if (verbose) { console.log(`Model response:\n${response.substring(0, 300)}...\n`); } // Parse response const parsed = emulator.parseResponse(response); if (parsed.finalAnswer) { finalAnswer = parsed.finalAnswer; lastReasoning = parsed.thought; if (verbose) { console.log('āœ… Received final answer, stopping loop.\n'); } break; } if (parsed.toolCall) { // Validate tool call const validation = emulator.validateToolCall(parsed.toolCall); if (!validation.valid) { if (verbose) { console.log(`āŒ Invalid tool call: ${validation.errors?.join(', ')}\n`); } // Append error as observation and retry prompt = emulator.appendObservation(prompt, `ERROR: ${validation.errors?.join('. ')}`); continue; } if (verbose) { console.log(`šŸ”Ø Executing tool: ${parsed.toolCall.name}`); console.log(` Arguments: ${JSON.stringify(parsed.toolCall.arguments)}\n`); } // Execute tool try { const result = await toolExecutor(parsed.toolCall); toolCalls.push(parsed.toolCall); if (verbose) { console.log(`āœ… Tool result: ${JSON.stringify(result).substring(0, 200)}\n`); } // Append observation prompt = emulator.appendObservation(prompt, JSON.stringify(result)); } catch (error) { if (verbose) { console.log(`āŒ Tool execution failed: ${error.message}\n`); } prompt = emulator.appendObservation(prompt, `ERROR: ${error.message}`); } lastReasoning = parsed.thought; } else if (parsed.textResponse) { // Model didn't use a tool and gave a direct response finalAnswer = parsed.textResponse; if (verbose) { console.log('šŸ“ Model provided direct text response (no tool use).\n'); } break; } } if (!finalAnswer && verbose) { console.log('āš ļø Reached max iterations without final answer.\n'); } const confidence = emulator.getConfidence({ toolCall: toolCalls[toolCalls.length - 1], finalAnswer, thought: lastReasoning }); return { toolCalls, reasoning: lastReasoning, finalAnswer, confidence }; } //# sourceMappingURL=tool-emulation.js.map