fix: refactor tool execution to use generateText with proper tool handling
Major changes: - Fixed package executor URL protocol handling (add https:// if missing) - Switched from streamText to generateText for proper tool execution - AI now calls tool AND generates natural language response - Tool results no longer show raw JSON metadata Tool executor (tool-executor-agent.ts): - Use generateText() instead of streamText() for tool execution - Add system prompt to guide AI to summarize tool results - Return result.text for natural language output - Tool definition uses inputSchema (AI SDK v6 format) Package executor: - Add getSandboxUrl() to ensure URL has https:// protocol - Fixes "Failed to parse URL" error in production 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
45da32e8ec
commit
47ddc4dea7
2 changed files with 36 additions and 75 deletions
|
|
@ -6,7 +6,7 @@
|
|||
import { openai } from '@ai-sdk/openai';
|
||||
import type { Tool } from '@tpmjs/db';
|
||||
import { executePackage } from '@tpmjs/package-executor';
|
||||
import { type CoreMessage, streamText } from 'ai';
|
||||
import { type CoreMessage, generateText } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
|
|
@ -101,20 +101,22 @@ export function createToolDefinition(tool: Tool) {
|
|||
console.log('[createToolDefinition] Parameters length:', parameters.length);
|
||||
|
||||
// Ensure we have a valid schema - if no parameters, use an empty object
|
||||
const schema =
|
||||
const inputSchema =
|
||||
parameters.length > 0
|
||||
? tpmjsParamsToZodSchema(parameters)
|
||||
: z.object({}).describe('No parameters required');
|
||||
|
||||
console.log('[createToolDefinition] Created Zod schema:', schema);
|
||||
console.log('[createToolDefinition] Schema type:', typeof schema);
|
||||
console.log('[createToolDefinition] Schema constructor:', schema.constructor.name);
|
||||
console.log('[createToolDefinition] Created Zod schema:', inputSchema);
|
||||
|
||||
// AI SDK v6 beta tool definition - uses inputSchema instead of parameters
|
||||
const sanitizedName = sanitizeToolName(tool.npmPackageName);
|
||||
|
||||
// AI SDK v6 tool definition
|
||||
return {
|
||||
description: tool.description,
|
||||
inputSchema: schema, // Changed from 'parameters' to 'inputSchema' in v6
|
||||
inputSchema, // AI SDK v6 uses inputSchema
|
||||
execute: async (params: Record<string, unknown>) => {
|
||||
console.log('[Tool execute] Running:', sanitizedName, params);
|
||||
|
||||
// Execute the actual npm package in a sandbox
|
||||
const result = await executePackage(
|
||||
tool.npmPackageName,
|
||||
|
|
@ -127,6 +129,7 @@ export function createToolDefinition(tool: Tool) {
|
|||
throw new Error(result.error || 'Package execution failed');
|
||||
}
|
||||
|
||||
console.log('[Tool execute] Result:', result.output);
|
||||
return result.output;
|
||||
},
|
||||
};
|
||||
|
|
@ -182,7 +185,7 @@ function sanitizeToolName(npmPackageName: string): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Execute tool with AI agent and streaming
|
||||
* Execute tool with AI agent using AI SDK v6
|
||||
*/
|
||||
export async function executeToolWithAgent(
|
||||
tool: Tool,
|
||||
|
|
@ -193,8 +196,7 @@ export async function executeToolWithAgent(
|
|||
const toolDef = createToolDefinition(tool);
|
||||
const sanitizedToolName = sanitizeToolName(tool.npmPackageName);
|
||||
|
||||
console.log('[executeToolWithAgent] Sanitized tool name:', sanitizedToolName);
|
||||
console.log('[executeToolWithAgent] Tool definition:', toolDef);
|
||||
console.log('[executeToolWithAgent] Tool name:', sanitizedToolName);
|
||||
|
||||
const messages: CoreMessage[] = [
|
||||
{
|
||||
|
|
@ -203,82 +205,30 @@ export async function executeToolWithAgent(
|
|||
},
|
||||
];
|
||||
|
||||
let fullOutput = '';
|
||||
let agentSteps = 0;
|
||||
|
||||
const toolsConfig = {
|
||||
[sanitizedToolName]: toolDef,
|
||||
};
|
||||
|
||||
console.log(
|
||||
'[executeToolWithAgent] Tools config being sent to OpenAI:',
|
||||
JSON.stringify(toolsConfig, null, 2)
|
||||
);
|
||||
console.log('[executeToolWithAgent] Calling generateText');
|
||||
|
||||
const result = await streamText({
|
||||
// Use generateText for tool execution
|
||||
const result = await generateText({
|
||||
model: openai('gpt-4-turbo'),
|
||||
messages,
|
||||
tools: toolsConfig,
|
||||
onFinish: () => {
|
||||
agentSteps++;
|
||||
console.log('[executeToolWithAgent] Stream finished, steps:', agentSteps);
|
||||
},
|
||||
system: `You are a helpful assistant. When the user asks you to do something, use the ${sanitizedToolName} tool to help them, then provide a clear, natural language summary of the results.`,
|
||||
});
|
||||
|
||||
console.log('[executeToolWithAgent] Starting text stream consumption');
|
||||
console.log('[executeToolWithAgent] Result:', JSON.stringify(result, null, 2));
|
||||
|
||||
// In AI SDK v6, we need to handle both text and tool calls
|
||||
// The response might be ONLY a tool call with no text
|
||||
let toolCallResult: unknown = null;
|
||||
// Extract the final text output
|
||||
const fullOutput = result.text || JSON.stringify(result, null, 2);
|
||||
|
||||
// Stream and collect text
|
||||
for await (const chunk of result.textStream) {
|
||||
console.log('[executeToolWithAgent] Received text chunk:', chunk);
|
||||
fullOutput += chunk;
|
||||
onChunk?.(chunk);
|
||||
}
|
||||
console.log('[executeToolWithAgent] Final output:', fullOutput);
|
||||
|
||||
console.log('[executeToolWithAgent] Text stream complete, fullOutput length:', fullOutput.length);
|
||||
|
||||
// Get the full response including tool calls
|
||||
const fullResponse = await result.response;
|
||||
console.log('[executeToolWithAgent] Full response:', JSON.stringify(fullResponse, null, 2));
|
||||
console.log('[executeToolWithAgent] Response keys:', Object.keys(fullResponse));
|
||||
|
||||
// In AI SDK v6, check the messages array for tool calls
|
||||
if (fullResponse.messages && fullResponse.messages.length > 0) {
|
||||
console.log('[executeToolWithAgent] Messages detected:', fullResponse.messages.length);
|
||||
|
||||
for (const message of fullResponse.messages) {
|
||||
console.log('[executeToolWithAgent] Message role:', message.role);
|
||||
console.log('[executeToolWithAgent] Message keys:', Object.keys(message));
|
||||
|
||||
// Tool results are in messages with role 'tool'
|
||||
if (message.role === 'tool' && 'content' in message) {
|
||||
toolCallResult = message.content;
|
||||
console.log('[executeToolWithAgent] Tool message result:', toolCallResult);
|
||||
|
||||
// If there's no text output, use the tool result as the output
|
||||
if (fullOutput.length === 0 && toolCallResult) {
|
||||
fullOutput =
|
||||
typeof toolCallResult === 'string'
|
||||
? toolCallResult
|
||||
: JSON.stringify(toolCallResult, null, 2);
|
||||
|
||||
// Stream the tool result
|
||||
onChunk?.(fullOutput);
|
||||
console.log(
|
||||
'[executeToolWithAgent] Using tool result as output, length:',
|
||||
fullOutput.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check assistant messages for tool calls
|
||||
if (message.role === 'assistant' && 'toolCalls' in message) {
|
||||
console.log('[executeToolWithAgent] Assistant message has toolCalls:', message.toolCalls);
|
||||
}
|
||||
}
|
||||
// Stream the output (all at once since generateText is non-streaming)
|
||||
if (onChunk) {
|
||||
onChunk(fullOutput);
|
||||
}
|
||||
|
||||
// Calculate final token breakdown
|
||||
|
|
@ -298,6 +248,6 @@ export async function executeToolWithAgent(
|
|||
return {
|
||||
output: fullOutput,
|
||||
tokenBreakdown,
|
||||
agentSteps,
|
||||
agentSteps: result.steps?.length || 1,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,18 @@
|
|||
import type { ExecutionResult, ExecutorOptions } from './types.js';
|
||||
|
||||
const DEFAULT_TIMEOUT = 10000; // 10 seconds
|
||||
const SANDBOX_URL = process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3000';
|
||||
|
||||
// Ensure URL has protocol
|
||||
function getSandboxUrl(): string {
|
||||
const url = process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3000';
|
||||
// If URL doesn't start with http:// or https://, add https://
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
const SANDBOX_URL = getSandboxUrl();
|
||||
|
||||
/**
|
||||
* Execute a package function with parameters via remote sandbox
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue