Compare commits
2 Commits
main
...
bugfix/Zod
| Author | SHA1 | Date |
|---|---|---|
|
|
5ac1488a18 | |
|
|
02bab45d60 |
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T extends z.ZodTypeAny>(schema: T, arg: unknown, maxDepth: number = 10): Promise<z.infer<T>> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue