diff --git a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts index fbf3965d..4fd9e710 100644 --- a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts +++ b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts @@ -4,7 +4,13 @@ import { RunnableConfig } from '@langchain/core/runnables' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' import { StructuredTool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { + getCredentialData, + getCredentialParam, + executeJavaScriptCode, + createCodeExecutionSandbox, + parseWithTypeConversion +} from '../../../src/utils' import { isValidUUID, isValidURL } from '../../../src/validator' import { v4 as uuidv4 } from 'uuid' @@ -273,7 +279,7 @@ class AgentflowTool extends StructuredTool { } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new Error(`Received tool input did not match expected schema: ${JSON.stringify(arg)}`) } diff --git a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts index b63d7b6a..a2db7fbf 100644 --- a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts +++ b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts @@ -4,7 +4,13 @@ import { RunnableConfig } from '@langchain/core/runnables' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' import { StructuredTool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { + getCredentialData, + getCredentialParam, + executeJavaScriptCode, + createCodeExecutionSandbox, + parseWithTypeConversion +} from '../../../src/utils' import { isValidUUID, isValidURL } from '../../../src/validator' import { v4 as uuidv4 } from 'uuid' @@ -281,7 +287,7 @@ class ChatflowTool extends StructuredTool { } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new Error(`Received tool input did not match expected schema: ${JSON.stringify(arg)}`) } diff --git a/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts b/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts index 5a03ce18..544ed856 100644 --- a/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts +++ b/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts @@ -1,5 +1,5 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { getBaseClasses, getCredentialData, getCredentialParam, parseWithTypeConversion } from '../../../src/utils' import { StructuredTool, ToolInputParsingException, ToolParams } from '@langchain/core/tools' import { Sandbox } from '@e2b/code-interpreter' import { z } from 'zod' @@ -159,7 +159,7 @@ export class E2BTool extends StructuredTool { } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg)) } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 9b899f76..06ebd3e2 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -2,7 +2,7 @@ import { z } from 'zod' import { RunnableConfig } from '@langchain/core/runnables' import { StructuredTool, ToolParams } from '@langchain/core/tools' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' -import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { executeJavaScriptCode, createCodeExecutionSandbox, parseWithTypeConversion } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' class ToolInputParsingException extends Error { @@ -68,7 +68,7 @@ export class DynamicStructuredTool< } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg)) } diff --git a/packages/components/nodes/tools/OpenAPIToolkit/core.ts b/packages/components/nodes/tools/OpenAPIToolkit/core.ts index 5eb329fe..ebfc8c14 100644 --- a/packages/components/nodes/tools/OpenAPIToolkit/core.ts +++ b/packages/components/nodes/tools/OpenAPIToolkit/core.ts @@ -3,7 +3,7 @@ import { RequestInit } from 'node-fetch' import { RunnableConfig } from '@langchain/core/runnables' import { StructuredTool, ToolParams } from '@langchain/core/tools' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' -import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { executeJavaScriptCode, createCodeExecutionSandbox, parseWithTypeConversion } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' const removeNulls = (obj: Record) => { @@ -174,7 +174,7 @@ export class DynamicStructuredTool< } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new ToolInputParsingException(`Received tool input did not match expected schema ${e}`, JSON.stringify(arg)) } diff --git a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts index d95ee11d..f23701e6 100644 --- a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts +++ b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts @@ -3,7 +3,7 @@ import { CallbackManager, CallbackManagerForToolRun, Callbacks, parseCallbackCon import { BaseDynamicToolInput, DynamicTool, StructuredTool, ToolInputParsingException } from '@langchain/core/tools' import { BaseRetriever } from '@langchain/core/retrievers' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, resolveFlowObjValue } from '../../../src/utils' +import { getBaseClasses, resolveFlowObjValue, parseWithTypeConversion } from '../../../src/utils' import { SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents' import { RunnableConfig } from '@langchain/core/runnables' import { VectorStoreRetriever } from '@langchain/core/vectorstores' @@ -58,7 +58,7 @@ class DynamicStructuredTool = z.ZodObj } let parsed try { - parsed = await this.schema.parseAsync(arg) + parsed = await parseWithTypeConversion(this.schema, arg) } catch (e) { throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg)) } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 6405480e..7c526681 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -4,11 +4,11 @@ import * as fs from 'fs' import * as path from 'path' import { JSDOM } from 'jsdom' import { z } from 'zod' +import { cloneDeep, omit, get } from 'lodash' import TurndownService from 'turndown' import { DataSource, Equal } from 'typeorm' import { ICommonObject, IDatabaseEntity, IFileUpload, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface' import { AES, enc } from 'crypto-js' -import { omit, get } from 'lodash' import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages' import { Document } from '@langchain/core/documents' import { getFileFromStorage } from './storageUtils' @@ -1760,3 +1760,164 @@ export const parseJsonBody = (body: string): any => { } } } + +/** + * Parse a value against a Zod schema with automatic type conversion for common type mismatches + * @param schema - The Zod schema to parse against + * @param arg - The value to parse + * @param maxDepth - Maximum recursion depth to prevent infinite loops (default: 10) + * @returns The parsed value + * @throws Error if parsing fails after attempting type conversions + */ +export async function parseWithTypeConversion(schema: T, arg: unknown, maxDepth: number = 10): Promise> { + // Safety check: prevent infinite recursion + if (maxDepth <= 0) { + throw new Error('Maximum recursion depth reached in parseWithTypeConversion') + } + + try { + return await schema.parseAsync(arg) + } catch (e) { + // Check if it's a ZodError and try to fix type mismatches + if (z.ZodError && e instanceof z.ZodError) { + const zodError = e as z.ZodError + // Deep clone the arg to avoid mutating the original + const modifiedArg = typeof arg === 'object' && arg !== null ? cloneDeep(arg) : arg + let hasModification = false + + // Helper function to set a value at a nested path + const setValueAtPath = (obj: any, path: (string | number)[], value: any): void => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const key = path[i] + if (current && typeof current === 'object' && key in current) { + current = current[key] + } else { + return // Path doesn't exist + } + } + if (current !== undefined && current !== null) { + const finalKey = path[path.length - 1] + current[finalKey] = value + } + } + + // Helper function to get a value at a nested path + const getValueAtPath = (obj: any, path: (string | number)[]): any => { + let current = obj + for (const key of path) { + if (current && typeof current === 'object' && key in current) { + current = current[key] + } else { + return undefined + } + } + return current + } + + // Helper function to convert value to expected type + const convertValue = (value: any, expected: string, received: string): any => { + // Expected string + if (expected === 'string') { + if (received === 'object' || received === 'array') { + return JSON.stringify(value) + } + if (received === 'number' || received === 'boolean') { + return String(value) + } + } + // Expected number + else if (expected === 'number') { + if (received === 'string') { + const parsed = parseFloat(value) + if (!isNaN(parsed)) { + return parsed + } + } + if (received === 'boolean') { + return value ? 1 : 0 + } + } + // Expected boolean + else if (expected === 'boolean') { + if (received === 'string') { + const lower = String(value).toLowerCase().trim() + if (lower === 'true' || lower === '1' || lower === 'yes') { + return true + } + if (lower === 'false' || lower === '0' || lower === 'no') { + return false + } + } + if (received === 'number') { + return value !== 0 + } + } + // Expected object + else if (expected === 'object') { + if (received === 'string') { + try { + const parsed = JSON.parse(value) + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed + } + } catch { + // Invalid JSON, return undefined to skip conversion + } + } + } + // Expected array + else if (expected === 'array') { + if (received === 'string') { + try { + const parsed = JSON.parse(value) + if (Array.isArray(parsed)) { + return parsed + } + } catch { + // Invalid JSON, return undefined to skip conversion + } + } + if (received === 'object' && value !== null) { + // Convert object to array (e.g., {0: 'a', 1: 'b'} -> ['a', 'b']) + // Only if it looks like an array-like object + const keys = Object.keys(value) + const numericKeys = keys.filter((k) => /^\d+$/.test(k)) + if (numericKeys.length === keys.length) { + return numericKeys.map((k) => value[k]) + } + } + } + return undefined // No conversion possible + } + + // Process each issue in the error + for (const issue of zodError.issues) { + // Handle invalid_type errors (type mismatches) + if (issue.code === 'invalid_type' && issue.path.length > 0) { + try { + const valueAtPath = getValueAtPath(modifiedArg, issue.path) + if (valueAtPath !== undefined) { + const convertedValue = convertValue(valueAtPath, issue.expected, issue.received) + if (convertedValue !== undefined) { + setValueAtPath(modifiedArg, issue.path, convertedValue) + hasModification = true + } + } + } catch (pathError) { + console.error('Error processing path in Zod error', pathError) + } + } + } + + // If we modified the arg, recursively call parseWithTypeConversion + // This allows newly surfaced nested errors to also get conversion treatment + // Decrement maxDepth to prevent infinite recursion + if (hasModification) { + return await parseWithTypeConversion(schema, modifiedArg, maxDepth - 1) + } + } + // Re-throw the original error if not a ZodError or no conversion possible + throw e + } +}