diff --git a/src/services/llm/pipeline/chat_pipeline.ts b/src/services/llm/pipeline/chat_pipeline.ts index 0136be93c..eea21a4ed 100644 --- a/src/services/llm/pipeline/chat_pipeline.ts +++ b/src/services/llm/pipeline/chat_pipeline.ts @@ -314,21 +314,39 @@ export class ChatPipeline { log.info(`Tools enabled in options: ${toolsEnabled}`); log.info(`Response provider: ${currentResponse.provider || 'unknown'}`); log.info(`Response model: ${currentResponse.model || 'unknown'}`); - log.info(`Response has tool_calls: ${currentResponse.tool_calls ? 'true' : 'false'}`); - if (currentResponse.tool_calls) { - log.info(`Number of tool calls: ${currentResponse.tool_calls.length}`); - log.info(`Tool calls details: ${JSON.stringify(currentResponse.tool_calls)}`); - // Check if we have a response from Ollama, which might be handled differently - if (currentResponse.provider === 'Ollama') { - log.info(`ATTENTION: Response is from Ollama - checking if tool execution path is correct`); - log.info(`Tool calls type: ${typeof currentResponse.tool_calls}`); - log.info(`First tool call name: ${currentResponse.tool_calls[0]?.function?.name || 'unknown'}`); + // Enhanced tool_calls detection - check both direct property and getter + let hasToolCalls = false; + + // First check the direct property + if (currentResponse.tool_calls && currentResponse.tool_calls.length > 0) { + hasToolCalls = true; + log.info(`Response has tool_calls property with ${currentResponse.tool_calls.length} tools`); + log.info(`Tool calls details: ${JSON.stringify(currentResponse.tool_calls)}`); + } + // Check if it might be a getter (for dynamic tool_calls collection) + else { + try { + const toolCallsDesc = Object.getOwnPropertyDescriptor(currentResponse, 'tool_calls'); + if (toolCallsDesc && typeof toolCallsDesc.get === 'function') { + const dynamicToolCalls = toolCallsDesc.get.call(currentResponse); + if (dynamicToolCalls && dynamicToolCalls.length > 0) { + hasToolCalls = true; + log.info(`Response has dynamic tool_calls with ${dynamicToolCalls.length} tools`); + log.info(`Dynamic tool calls details: ${JSON.stringify(dynamicToolCalls)}`); + // Ensure property is available for subsequent code + currentResponse.tool_calls = dynamicToolCalls; + } + } + } catch (e) { + log.error(`Error checking dynamic tool_calls: ${e}`); } } + log.info(`Response has tool_calls: ${hasToolCalls ? 'true' : 'false'}`); + // Tool execution loop - if (toolsEnabled && currentResponse.tool_calls && currentResponse.tool_calls.length > 0) { + if (toolsEnabled && hasToolCalls && currentResponse.tool_calls) { log.info(`========== STAGE 6: TOOL EXECUTION ==========`); log.info(`Response contains ${currentResponse.tool_calls.length} tool calls, processing...`); diff --git a/src/services/llm/providers/anthropic_service.ts b/src/services/llm/providers/anthropic_service.ts index 5a4531b5a..e4087417d 100644 --- a/src/services/llm/providers/anthropic_service.ts +++ b/src/services/llm/providers/anthropic_service.ts @@ -186,10 +186,12 @@ export class AnthropicService extends BaseAIService { opts: ChatCompletionOptions, providerOptions: AnthropicOptions ): Promise { + // Create a list to collect tool calls during streaming + const collectedToolCalls: any[] = []; + // Create a stream handler function that processes the SDK's stream const streamHandler = async (callback: (chunk: StreamChunk) => Promise | void): Promise => { let completeText = ''; - const toolCalls: any[] = []; let currentToolCall: any = null; try { @@ -261,7 +263,7 @@ export class AnthropicService extends BaseAIService { // Process tool use completion else if (chunk.type === 'content_block_stop' && currentToolCall) { // Add the completed tool call to our list - toolCalls.push(currentToolCall); + collectedToolCalls.push({ ...currentToolCall }); // Log the tool completion log.info(`Streaming: Tool use completed: ${currentToolCall.function.name}`); @@ -274,6 +276,7 @@ export class AnthropicService extends BaseAIService { type: 'complete', tool: currentToolCall }, + tool_calls: collectedToolCalls.length > 0 ? collectedToolCalls : undefined, raw: chunk }); @@ -282,11 +285,16 @@ export class AnthropicService extends BaseAIService { } } - // Signal completion + // Signal completion with all tool calls + log.info(`Streaming complete, collected ${collectedToolCalls.length} tool calls`); + if (collectedToolCalls.length > 0) { + log.info(`Tool calls detected in final response: ${JSON.stringify(collectedToolCalls)}`); + } + await callback({ text: '', done: true, - tool_calls: toolCalls.length > 0 ? toolCalls : undefined + tool_calls: collectedToolCalls.length > 0 ? collectedToolCalls : undefined }); return completeText; @@ -311,13 +319,43 @@ export class AnthropicService extends BaseAIService { } }; - // Return a response object with the stream handler - return { + // Create a custom stream function that captures tool calls + const captureToolCallsStream = async (callback: (chunk: StreamChunk) => Promise | void): Promise => { + // Use the original stream handler but wrap it to capture tool calls + return streamHandler(async (chunk: StreamChunk) => { + // If the chunk has tool calls, update our collection + if (chunk.tool_calls && chunk.tool_calls.length > 0) { + // Update our collection with new tool calls + chunk.tool_calls.forEach(toolCall => { + // Only add if it's not already in the collection + if (!collectedToolCalls.some(tc => tc.id === toolCall.id)) { + collectedToolCalls.push(toolCall); + } + }); + } + + // Call the original callback + return callback(chunk); + }); + }; + + // Return a response object with the stream handler and tool_calls property + const response: ChatResponse = { text: '', // Initial text is empty, will be populated during streaming model: providerOptions.model, provider: this.getName(), - stream: streamHandler + stream: captureToolCallsStream }; + + // Define a getter for tool_calls that will return the collected tool calls + Object.defineProperty(response, 'tool_calls', { + get: function() { + return collectedToolCalls.length > 0 ? collectedToolCalls : undefined; + }, + enumerable: true + }); + + return response; } /**