diff --git a/app/api/execute/route.ts b/app/api/execute/route.ts index 4b363eba37..e33593f52f 100644 --- a/app/api/execute/route.ts +++ b/app/api/execute/route.ts @@ -12,7 +12,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json() - const { code, timeout = 3000 } = body + const { code, params = {}, timeout = 3000 } = body // Check if code contains unresolved template variables if (code.includes('<') && code.includes('>')) { @@ -23,6 +23,7 @@ export async function POST(req: NextRequest) { // Create a secure context with console logging const context = createContext({ + params, console: { log: (...args: any[]) => { const logMessage = @@ -42,7 +43,6 @@ export async function POST(req: NextRequest) { }, }) - // Create and run the script const script = new Script(` (async () => { try { diff --git a/executor/handlers.ts b/executor/handlers.ts index 682380d8c3..76ed543592 100644 --- a/executor/handlers.ts +++ b/executor/handlers.ts @@ -69,6 +69,22 @@ export class AgentBlockHandler implements BlockHandler { const formattedTools = Array.isArray(inputs.tools) ? inputs.tools .map((tool: any) => { + // Handle custom tools + if (tool.type === 'custom-tool' && tool.schema) { + return { + id: `custom_${tool.title}`, + name: tool.schema.function.name, + description: tool.schema.function.description || '', + params: tool.params || {}, + parameters: { + type: tool.schema.function.parameters.type, + properties: tool.schema.function.parameters.properties, + required: tool.schema.function.parameters.required || [], + }, + } + } + + // Handle regular block tools const blockFound = getAllBlocks().find((b) => b.type === tool.type) const toolId = blockFound?.tools.access[0] if (!toolId) return null diff --git a/providers/utils.ts b/providers/utils.ts index b5b93f8cf0..de1f6b228a 100644 --- a/providers/utils.ts +++ b/providers/utils.ts @@ -1,9 +1,10 @@ +import { useCustomToolsStore } from '@/stores/custom-tools/store' import { anthropicProvider } from './anthropic' import { cerebrasProvider } from './cerebras' import { deepseekProvider } from './deepseek' import { googleProvider } from './google' import { openaiProvider } from './openai' -import { ProviderConfig, ProviderId } from './types' +import { ProviderConfig, ProviderId, ProviderToolConfig } from './types' import { xAIProvider } from './xai' /** @@ -192,3 +193,37 @@ export function extractAndParseJSON(content: string): any { } } } + +/** + * Transforms a custom tool schema into a provider tool config + */ +export function transformCustomTool(customTool: any): ProviderToolConfig { + const schema = customTool.schema + + if (!schema || !schema.function) { + throw new Error('Invalid custom tool schema') + } + + return { + id: `custom_${customTool.id}`, // Prefix with 'custom_' to identify custom tools + name: schema.function.name, + description: schema.function.description || '', + params: {}, // This will be derived from parameters + parameters: { + type: schema.function.parameters.type, + properties: schema.function.parameters.properties, + required: schema.function.parameters.required || [], + }, + } +} + +/** + * Gets all available custom tools as provider tool configs + */ +export function getCustomTools(): ProviderToolConfig[] { + // Get custom tools from the store + const customTools = useCustomToolsStore.getState().getAllTools() + + // Transform each custom tool into a provider tool config + return customTools.map(transformCustomTool) +} diff --git a/providers/xai/index.ts b/providers/xai/index.ts index 466882a40b..f6cb603b7f 100644 --- a/providers/xai/index.ts +++ b/providers/xai/index.ts @@ -1,4 +1,5 @@ import OpenAI from 'openai' +import { executeTool } from '@/tools' import { ProviderConfig, ProviderRequest, ProviderResponse } from '../types' export const xAIProvider: ProviderConfig = { @@ -141,13 +142,7 @@ export const xAIProvider: ProviderConfig = { const tool = request.tools?.find((t) => t.id === toolName) if (!tool) continue - const { executeTool } = await import('@/tools') const mergedArgs = { ...tool.params, ...toolArgs } - console.log(`Merged tool args for ${toolName}:`, { - toolParams: tool.params, - llmArgs: toolArgs, - mergedArgs, - }) const result = await executeTool(toolName, mergedArgs, true) if (!result.success) continue diff --git a/serializer/index.ts b/serializer/index.ts index bd64aa7d04..120459112b 100644 --- a/serializer/index.ts +++ b/serializer/index.ts @@ -28,13 +28,36 @@ export class Serializer { throw new Error(`Invalid block type: ${block.type}`) } - // Get tool ID from block config - const toolId = blockConfig.tools.config?.tool - ? blockConfig.tools.config.tool(this.extractParams(block)) - : blockConfig.tools.access[0] - - // Extract params from subBlocks + // Check if this is an agent block with custom tools const params = this.extractParams(block) + let toolId = '' + + if (block.type === 'agent' && params.tools) { + // Process the tools in the agent block + try { + const tools = Array.isArray(params.tools) ? params.tools : JSON.parse(params.tools) + + // If there are custom tools, we just keep them as is + // They'll be handled by the executor during runtime + + // For non-custom tools, we determine the tool ID + const nonCustomTools = tools.filter((tool: any) => tool.type !== 'custom-tool') + if (nonCustomTools.length > 0) { + toolId = blockConfig.tools.config?.tool + ? blockConfig.tools.config.tool(params) + : blockConfig.tools.access[0] + } + } catch (error) { + console.error('Error processing tools in agent block:', error) + // Default to the first tool if we can't process tools + toolId = blockConfig.tools.access[0] + } + } else { + // For non-agent blocks, get tool ID from block config as usual + toolId = blockConfig.tools.config?.tool + ? blockConfig.tools.config.tool(params) + : blockConfig.tools.access[0] + } // Get inputs from block config const inputs: Record = {} diff --git a/tools/index.ts b/tools/index.ts index 9ba7efa707..2ab3b8197d 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -1,3 +1,4 @@ +import { useCustomToolsStore } from '@/stores/custom-tools/store' import { visionTool as crewAIVision } from './crewai/vision' import { scrapeTool } from './firecrawl/scrape' import { functionExecuteTool as functionExecute } from './function/execute' @@ -67,7 +68,94 @@ export const tools: Record = { // Get a tool by its ID export function getTool(toolId: string): ToolConfig | undefined { - return tools[toolId] + // Check for built-in tools + const builtInTool = tools[toolId] + if (builtInTool) return builtInTool + + // Check if it's a custom tool + if (toolId.startsWith('custom_')) { + return getCustomTool(toolId) + } + + return undefined +} + +// Create a tool config from a custom tool definition +function getCustomTool(customToolId: string): ToolConfig | undefined { + // Extract the identifier part (could be UUID or title) + const identifier = customToolId.replace('custom_', '') + + const customToolsStore = useCustomToolsStore.getState() + + // Try to find the tool directly by ID first + let customTool = customToolsStore.getTool(identifier) + + // If not found by ID, try to find by title (for backward compatibility) + if (!customTool) { + const allTools = customToolsStore.getAllTools() + customTool = allTools.find((tool) => tool.title === identifier) + } + + if (!customTool) { + console.error(`Custom tool not found: ${identifier}`) + return undefined + } + + // Create a parameter schema from the custom tool schema + const params: Record = {} + + if (customTool.schema.function?.parameters?.properties) { + Object.entries(customTool.schema.function.parameters.properties).forEach(([key, config]) => { + params[key] = { + type: config.type || 'string', + required: customTool.schema.function.parameters.required?.includes(key) || false, + requiredForToolCall: customTool.schema.function.parameters.required?.includes(key) || false, + description: config.description || '', + } + }) + } + + // Create a tool config for the custom tool + return { + id: customToolId, + name: customTool.title, + description: customTool.schema.function?.description || '', + version: '1.0.0', + params, + + // Request configuration - for custom tools we'll use the execute endpoint + request: { + url: '/api/execute', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params: Record) => { + // Include everything needed for execution + return { + code: customTool.code, + params: params, // These will be available in the VM context + schema: customTool.schema.function.parameters, // For validation on the client side + } + }, + isInternalRoute: true, + }, + + // Response handling + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.success) { + throw new Error(data.error || 'Custom tool execution failed') + } + + return { + success: true, + output: data.output.result || data.output, + error: undefined, + } + }, + transformError: (error: any) => + `Custom tool execution error: ${error.message || 'Unknown error'}`, + } } // Execute a tool by calling either the proxy for external APIs or directly for internal routes @@ -78,10 +166,6 @@ export async function executeTool( ): Promise { try { const tool = getTool(toolId) - console.log(`Tool being called: ${toolId}`, { - params: { ...params, apiKey: params.apiKey ? '[REDACTED]' : undefined }, - skipProxy, - }) // Validate the tool and its parameters validateToolRequest(toolId, tool, params) @@ -93,21 +177,25 @@ export async function executeTool( // For internal routes or when skipProxy is true, call the API directly if (tool.request.isInternalRoute || skipProxy) { - console.log(`Calling internal request for ${toolId}`) const result = await handleInternalRequest(toolId, tool, params) - console.log(`Tool ${toolId} execution result:`, { - success: result.success, - outputKeys: result.success ? Object.keys(result.output) : [], - error: result.error, - }) return result } // For external APIs, use the proxy - console.log(`Calling proxy request for ${toolId}`) return await handleProxyRequest(toolId, params) } catch (error: any) { console.error(`Error executing tool ${toolId}:`, error) + + // For custom tools, provide more helpful error information + if (toolId.startsWith('custom_')) { + const identifier = toolId.replace('custom_', '') + const allTools = useCustomToolsStore.getState().getAllTools() + const availableTools = allTools.map((t) => ({ id: t.id, title: t.title })) + + console.error('Available custom tools:', availableTools) + console.error(`Looking for custom tool with identifier: ${identifier}`) + } + return { success: false, output: {}, @@ -124,17 +212,113 @@ async function handleInternalRequest( tool: ToolConfig, params: Record ): Promise { - // Log the request for debugging - console.log(`Executing tool ${toolId} with params:`, { - toolId, - params: { ...params, apiKey: params.apiKey ? '[REDACTED]' : undefined }, - }) - // Format the request parameters const requestParams = formatRequestParams(tool, params) - // Execute the request - return await executeRequest(toolId, tool, requestParams) + try { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || '' + // Handle the case where url may be a function or string + const endpointUrl = + typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url + + const fullUrl = new URL(endpointUrl, baseUrl).toString() + + // For custom tools, validate parameters on the client side before sending + if (toolId.startsWith('custom_') && tool.request.body) { + const requestBody = tool.request.body(params) + if (requestBody.schema && requestBody.params) { + validateClientSideParams(requestBody.params, requestBody.schema) + } + } + + const response = await fetch(fullUrl, { + method: requestParams.method, + headers: requestParams.headers, + body: requestParams.body, + }) + + if (!response.ok) { + let errorData + try { + errorData = await response.json() + } catch (e) { + errorData = { error: response.statusText } + } + + throw new Error(errorData.error || `Request failed with status ${response.status}`) + } + + // Use the tool's response transformer if available + if (tool.transformResponse) { + return await tool.transformResponse(response) + } + + // Default response handling + const data = await response.json() + return { + success: true, + output: data.output || data, + error: undefined, + } + } catch (error: any) { + // Use the tool's error transformer if available + if (tool.transformError) { + return { + success: false, + output: {}, + error: tool.transformError(error), + } + } + + return { + success: false, + output: {}, + error: error.message || 'Request failed', + } + } +} + +/** + * Validates parameters on the client side before sending to the execute endpoint + */ +function validateClientSideParams( + params: Record, + schema: { type: string; properties: Record; required?: string[] } +) { + if (!schema || schema.type !== 'object') { + throw new Error('Invalid schema format') + } + + // Check required parameters + if (schema.required) { + for (const requiredParam of schema.required) { + if (!(requiredParam in params)) { + throw new Error(`Required parameter missing: ${requiredParam}`) + } + } + } + + // Check parameter types (basic validation) + for (const [paramName, paramValue] of Object.entries(params)) { + const paramSchema = schema.properties[paramName] + if (!paramSchema) { + throw new Error(`Unknown parameter: ${paramName}`) + } + + // Basic type checking + const type = paramSchema.type + if (type === 'string' && typeof paramValue !== 'string') { + throw new Error(`Parameter ${paramName} should be a string`) + } else if (type === 'number' && typeof paramValue !== 'number') { + throw new Error(`Parameter ${paramName} should be a number`) + } else if (type === 'boolean' && typeof paramValue !== 'boolean') { + throw new Error(`Parameter ${paramName} should be a boolean`) + } else if (type === 'array' && !Array.isArray(paramValue)) { + throw new Error(`Parameter ${paramName} should be an array`) + } else if (type === 'object' && (typeof paramValue !== 'object' || paramValue === null)) { + throw new Error(`Parameter ${paramName} should be an object`) + } + } } /** diff --git a/tools/utils.ts b/tools/utils.ts index 325696ae4e..209402b473 100644 --- a/tools/utils.ts +++ b/tools/utils.ts @@ -66,14 +66,8 @@ export async function executeRequest( try { const { url, method, headers, body } = requestParams - // Log the request for debugging - console.log(`Executing tool ${toolId}:`, { url, method }) - const externalResponse = await fetch(url, { method, headers, body }) - // Log response status - console.log(`${toolId} response status:`, externalResponse.status, externalResponse.statusText) - if (!externalResponse.ok) { let errorContent try {