@gork-labs/secondbrain-mcp
Version:
Second Brain MCP Server - Agent team orchestration with dynamic tool discovery
456 lines (455 loc) • 19.5 kB
JavaScript
/**
* OpenAI Native Function Calling Implementation
*
* This implementation uses OpenAI's built-in function calling capabilities to replace
* the regex-based tool parsing system with reliable, structured function execution.
*
* Key Ben sessionLog.info('Executing sub-agent with OpenRouter native function calling', {
chatmode: chatmodeName,
availableTools: this.mcpTools.map(t => t.name),
functionToolsCount: functionTools.length,
maxIterations: this.maxIterations,
model: config.model
}); * 1. Structured function definitions with automatic validation
* 2. Built-in JSON parsing and type safety
* 3. Multiple simultaneous function calls support
* 4. Automatic error handling and retry logic
* 5. No regex parsing - uses OpenAI's proven approach
*
* Created: 2025-07-25T17:04:49+02:00
* Author: Staff Software Engineer - Gorka
*/
import OpenAI from 'openai';
import { logger } from '../utils/logger.js';
import { getVersionString } from '../utils/version.js';
import { config } from '../utils/config.js';
import { ResponseParser } from '../utils/response-parser.js';
/**
* Enhanced sub-agent executor using OpenAI's native function calling
* Replaces regex-based tool parsing with structured function execution
*/
export class OpenAIFunctionCallingExecutor {
openai;
mcpTools;
maxIterations;
toolNameMapping;
constructor(apiKey, mcpTools, maxIterations = 20) {
// Initialize OpenAI client with OpenRouter configuration
const versionString = getVersionString();
this.openai = new OpenAI({
apiKey: config.openrouterApiKey,
baseURL: 'https://openrouter.ai/api/v1',
defaultHeaders: {
'User-Agent': `${versionString} (SecondBrain-MCP)`,
'X-Client-Version': versionString,
'HTTP-Referer': 'https://github.com/gork-labs/gorka',
'X-Title': 'Gorka SecondBrain MCP'
}
});
this.mcpTools = mcpTools.filter(tool => tool.safe); // Only safe tools
this.maxIterations = maxIterations;
this.toolNameMapping = this.createToolNameMapping();
logger.info('OpenAI Function Calling Executor initialized', {
version: versionString,
safeToolsCount: this.mcpTools.length,
maxIterations: this.maxIterations
});
}
/**
* Create mapping from VS Code tool names to MCP tool names
* This enables backward compatibility with existing agent instructions
*/
createToolNameMapping() {
const mapping = new Map();
// File operations mapping
mapping.set('codebase', 'read_file');
mapping.set('read_file', 'read_file');
mapping.set('editFiles', 'write_file');
mapping.set('write_file', 'write_file');
mapping.set('search', 'search_files');
mapping.set('search_files', 'search_files');
mapping.set('file_search', 'search_files');
mapping.set('list_dir', 'list_directory');
mapping.set('list_directory', 'list_directory');
mapping.set('get_file_info', 'get_file_info');
// Git operations (these should already work)
mapping.set('git_status', 'git_status');
mapping.set('git_diff', 'git_diff');
mapping.set('git_log', 'git_log');
mapping.set('git_show', 'git_show');
// Memory operations (these should already work)
mapping.set('create_entities', 'create_entities');
mapping.set('search_nodes', 'search_nodes');
mapping.set('add_observations', 'add_observations');
// Sequential thinking (this should already work)
mapping.set('sequentialthinking', 'sequentialthinking');
// Time operations (these should already work)
mapping.set('get_current_time', 'get_current_time');
return mapping;
}
/**
* Map tool names and parameters from VS Code format to MCP format
*/
mapToolCall(toolName, args) {
const mappedToolName = this.toolNameMapping.get(toolName) || toolName;
let mappedArgs = { ...args };
// Handle parameter mapping for specific tools
switch (toolName) {
case 'codebase':
// VS Code codebase tool -> MCP read_file tool
if (args.filePath) {
mappedArgs = {
path: args.filePath,
startLine: args.startLine || 1,
endLine: args.endLine || 100
};
}
break;
case 'editFiles':
// VS Code editFiles tool -> MCP write_file tool
if (args.filePath && args.content) {
mappedArgs = {
path: args.filePath,
content: args.content
};
}
break;
case 'search':
case 'file_search':
// VS Code search tools -> MCP search_files tool
if (args.query) {
mappedArgs = {
pattern: args.query,
path: args.includePattern || '.',
caseSensitive: args.caseSensitive || false
};
}
break;
case 'list_dir':
// VS Code list_dir -> MCP list_directory
if (args.path) {
mappedArgs = {
path: args.path
};
}
break;
default:
// For all other tools, use args as-is
mappedArgs = args;
break;
}
return {
toolName: mappedToolName,
args: mappedArgs
};
}
createOpenAIFunctionTools() {
return this.mcpTools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: this.convertMCPSchemaToOpenAI(tool.inputSchema)
}
}));
}
/**
* Convert MCP input schema to OpenAI function parameters schema
*/
convertMCPSchemaToOpenAI(mcpSchema) {
// Handle common MCP schema patterns and convert to OpenAI format
if (!mcpSchema || typeof mcpSchema !== 'object') {
return {
type: 'object',
properties: {},
required: []
};
}
// If it's already in JSON Schema format, use it directly
if (mcpSchema.type === 'object' && mcpSchema.properties) {
return mcpSchema;
}
// Convert common patterns
return {
type: 'object',
properties: mcpSchema.properties || {},
required: mcpSchema.required || []
};
}
/**
* Execute sub-agent with native OpenAI function calling
*/
async executeSubAgent(instructions, chatmodeName, sessionId, toolExecutor) {
const sessionLog = logger.withSession(sessionId);
const startTime = performance.now();
let toolCallCount = 0;
try {
// Prepare OpenAI function tools
const functionTools = this.createOpenAIFunctionTools();
sessionLog.info('Executing sub-agent with OpenAI native function calling', {
chatmode: chatmodeName,
availableTools: this.mcpTools.map(t => t.name),
functionToolsCount: functionTools.length,
maxIterations: this.maxIterations,
model: config.model
});
const { result, toolCallCount: executionToolCalls } = await this.executeSubAgentManual(instructions, chatmodeName, sessionId, toolExecutor);
toolCallCount = executionToolCalls;
// Record successful execution metrics
const duration = performance.now() - startTime;
FunctionCallingMetrics.recordExecution(chatmodeName, duration, true, toolCallCount);
return result;
}
catch (error) {
// Record failed execution metrics
const duration = performance.now() - startTime;
const errorType = error instanceof Error ? error.constructor.name : 'UnknownError';
FunctionCallingMetrics.recordExecution(chatmodeName, duration, false, toolCallCount, errorType);
sessionLog.error('OpenAI sub-agent execution failed', {
chatmode: chatmodeName,
error: error instanceof Error ? error.message : String(error),
duration,
toolCallCount
});
throw error;
}
}
/**
* Manual function call handling implementation
* This provides full control over the execution flow
*/
async executeSubAgentManual(instructions, chatmodeName, sessionId, toolExecutor) {
const sessionLog = logger.withSession(sessionId);
const functionTools = this.createOpenAIFunctionTools();
let totalToolCalls = 0;
const messages = [
{
role: 'system',
content: instructions
}
];
let iteration = 0;
let consecutiveFailures = 0;
while (iteration < this.maxIterations) {
iteration++;
try {
const completion = await this.openai.chat.completions.create({
model: config.model,
messages,
tools: functionTools,
tool_choice: 'auto',
temperature: 0.7,
max_tokens: 4000
});
const message = completion.choices[0]?.message;
if (!message) {
throw new Error('Empty response from OpenAI');
}
// Add assistant message to conversation
messages.push(message);
// Check if there are tool calls to execute
if (message.tool_calls && message.tool_calls.length > 0) {
totalToolCalls += message.tool_calls.length;
sessionLog.info('Processing OpenAI function calls', {
toolCallCount: message.tool_calls.length,
totalToolCalls,
iteration
});
// Execute all tool calls
const toolResults = await Promise.all(message.tool_calls.map(async (toolCall) => {
if (toolCall.type !== 'function') {
return null;
}
const functionName = toolCall.function.name;
const functionArgs = JSON.parse(toolCall.function.arguments);
// Map tool name and arguments from VS Code format to MCP format
const { toolName: mappedToolName, args: mappedArgs } = this.mapToolCall(functionName, functionArgs);
sessionLog.info('Executing function call', {
originalFunctionName: functionName,
mappedFunctionName: mappedToolName,
callId: toolCall.id,
originalArguments: functionArgs,
mappedArguments: mappedArgs,
iteration
});
try {
const result = await toolExecutor(mappedToolName, mappedArgs);
consecutiveFailures = 0; // Reset on success
sessionLog.info('Function call executed successfully', {
originalFunctionName: functionName,
mappedFunctionName: mappedToolName,
callId: toolCall.id,
success: result.success,
serverId: result.serverId,
iteration
});
return {
tool_call_id: toolCall.id,
role: 'tool',
content: result.success
? JSON.stringify(result.content)
: `Error: ${result.error || 'Unknown error'}`
};
}
catch (error) {
consecutiveFailures++;
sessionLog.error('Function call execution failed', {
originalFunctionName: functionName,
mappedFunctionName: mappedToolName,
callId: toolCall.id,
error: error instanceof Error ? error.message : String(error),
consecutiveFailures,
iteration
});
// Safety check for consecutive failures
if (consecutiveFailures >= 5) {
throw new Error(`Sub-agent safety triggered: ${consecutiveFailures} consecutive function call failures. ` +
`Last failed function: "${functionName}" (mapped to "${mappedToolName}"). ` +
`Session: ${sessionId}, Iteration: ${iteration}/${this.maxIterations}.`);
}
return {
tool_call_id: toolCall.id,
role: 'tool',
content: `Error executing ${functionName}: ${error instanceof Error ? error.message : String(error)}`
};
}
}));
// Add tool results to conversation
const validResults = toolResults.filter(Boolean);
messages.push(...validResults);
// Continue the conversation
continue;
}
// No tool calls - this is the final response
sessionLog.info('OpenAI sub-agent completed', {
chatmode: chatmodeName,
iterations: iteration,
totalToolCalls,
finalResponseLength: message.content?.length || 0
});
const result = await this.parseSubAgentResponse(message.content || '', chatmodeName);
return { result, toolCallCount: totalToolCalls };
}
catch (error) {
sessionLog.error('OpenAI completion failed', {
iteration,
totalToolCalls,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
throw new Error(`Sub-agent exceeded maximum iterations (${this.maxIterations}). ` +
`Session: ${sessionId}, Final iteration: ${iteration}, Tool calls: ${totalToolCalls}.`);
}
/**
* Parse sub-agent response from final content using centralized parser
*/
async parseSubAgentResponse(responseText, chatmodeName) {
try {
return await ResponseParser.parseWithRetry(responseText, chatmodeName, {
maxRetries: 1 // Function calling context uses fewer retries
});
}
catch (error) {
logger.error('OpenAI function calling response parsing failed', {
chatmode: chatmodeName,
error: error instanceof Error ? error.message : String(error),
responseLength: responseText.length
});
// This should rarely happen with the new parser, but provide final fallback
throw new Error(`Failed to parse sub-agent response: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Configuration for OpenAI function calling
*/
export class FunctionCallingConfig {
/**
* Get OpenAI API key from environment
*/
static getOpenAIApiKey() {
const apiKey = process.env.OPENAI_API_KEY || '';
if (!apiKey) {
throw new Error('OPENAI_API_KEY environment variable is required');
}
return apiKey;
}
/**
* Check if chatmode should use OpenAI function calling
* Optional chatmode filtering via environment variable
*/
static shouldUseOpenAIFunctionCalling(chatmode) {
const supportedChatmodes = process.env.SECONDBRAIN_FUNCTION_CALLING_CHATMODES?.split(',').map(s => s.trim()) || [];
// If no specific chatmodes configured, enable for all
if (supportedChatmodes.length === 0) {
return true;
}
return supportedChatmodes.includes(chatmode);
}
}
/**
* Performance monitoring for OpenAI function calling
*/
export class FunctionCallingMetrics {
static metrics = new Map();
static recordExecution(chatmode, duration, success, toolCallCount, errorType) {
if (!this.metrics.has(chatmode)) {
this.metrics.set(chatmode, []);
}
this.metrics.get(chatmode).push({
duration,
success,
toolCallCount,
errorType,
timestamp: Date.now()
});
// Keep only last 100 entries per chatmode to prevent memory bloat
const chatmodeMetrics = this.metrics.get(chatmode);
if (chatmodeMetrics.length > 100) {
chatmodeMetrics.splice(0, chatmodeMetrics.length - 100);
}
}
static getMetrics(chatmode) {
if (chatmode) {
return this.metrics.get(chatmode) || [];
}
const allMetrics = Array.from(this.metrics.entries()).map(([mode, data]) => ({
chatmode: mode,
executions: data
}));
return allMetrics;
}
static getPerformanceSummary() {
const allData = Array.from(this.metrics.values()).flat();
const now = Date.now();
const last24Hours = allData.filter(d => (now - d.timestamp) < (24 * 60 * 60 * 1000));
return {
avgDuration: allData.length > 0
? allData.reduce((sum, d) => sum + d.duration, 0) / allData.length
: 0,
successRate: allData.length > 0
? allData.filter(d => d.success).length / allData.length
: 0,
totalExecutions: allData.length,
last24Hours: {
avgDuration: last24Hours.length > 0
? last24Hours.reduce((sum, d) => sum + d.duration, 0) / last24Hours.length
: 0,
successRate: last24Hours.length > 0
? last24Hours.filter(d => d.success).length / last24Hours.length
: 0,
executions: last24Hours.length
}
};
}
static clearMetrics(chatmode) {
if (chatmode) {
this.metrics.delete(chatmode);
}
else {
this.metrics.clear();
}
}
}