implement parseWithTypeConversion - parse a value against a Zod schema with automatic type conversion for common type mismatches

This commit is contained in:
Henry 2025-10-31 12:32:17 +00:00
parent f3d5b7766d
commit 02bab45d60
7 changed files with 183 additions and 13 deletions

View File

@ -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)}`)
}

View File

@ -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)}`)
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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<string, any>) => {
@ -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))
}

View File

@ -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<T extends z.ZodObject<any, any, any, any> = 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))
}

View File

@ -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,161 @@ 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
* @returns The parsed value
* @throws Error if parsing fails after attempting type conversions
*/
export async function parseWithTypeConversion<T extends z.ZodTypeAny>(schema: T, arg: unknown): Promise<z.infer<T>> {
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, try parsing again
if (hasModification) {
try {
return await schema.parseAsync(modifiedArg)
} catch (e2) {
// Re-throw the original error after failed conversion attempt
throw e
}
}
}
// Re-throw the original error if not a ZodError or no conversion possible
throw e
}
}