diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts index 1a014933bf..0a71034fa5 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts @@ -115,6 +115,73 @@ describe('createNodeAsTool', () => { expect(tool.schema.shape.booleanWithDefault.description).toBe('Boolean with default'); }); + it('should allow omitting parameters with default values', () => { + node.parameters = { + requiredParam: "={{ $fromAI('requiredParam', 'Required parameter', 'string') }}", + optionalParam: + "={{ $fromAI('optionalParam', 'Optional parameter', 'string', 'default value') }}", + optionalNumber: "={{ $fromAI('optionalNumber', 'Optional number', 'number', 42) }}", + }; + + const tool = createNodeAsTool(options).response; + + // Test that the schema accepts an object with only the required field + // This should NOT throw an error if fields with defaults are truly optional + const parseResult = tool.schema.safeParse({ requiredParam: 'test' }); + + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.requiredParam).toBe('test'); + expect(parseResult.data.optionalParam).toBe('default value'); + expect(parseResult.data.optionalNumber).toBe(42); + } + + // Test that all fields can still be provided + const fullParseResult = tool.schema.safeParse({ + requiredParam: 'test', + optionalParam: 'custom value', + optionalNumber: 100, + }); + + expect(fullParseResult.success).toBe(true); + if (fullParseResult.success) { + expect(fullParseResult.data.requiredParam).toBe('test'); + expect(fullParseResult.data.optionalParam).toBe('custom value'); + expect(fullParseResult.data.optionalNumber).toBe(100); + } + }); + + it('should allow omitting parameters with default values = empty string', () => { + node.parameters = { + requiredParam: "={{ $fromAI('requiredParam', 'Required parameter', 'string') }}", + optionalParam: "={{ $fromAI('optionalParam', 'Optional parameter', 'string', '') }}", + }; + + const tool = createNodeAsTool(options).response; + + // Test that the schema accepts an object with only the required field + // This should NOT throw an error if fields with defaults are truly optional + const parseResult = tool.schema.safeParse({ requiredParam: 'test' }); + + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.requiredParam).toBe('test'); + expect(parseResult.data.optionalParam).toBe(''); + } + + // Test that all fields can still be provided + const fullParseResult = tool.schema.safeParse({ + requiredParam: 'test', + optionalParam: 'custom value', + }); + + expect(fullParseResult.success).toBe(true); + if (fullParseResult.success) { + expect(fullParseResult.data.requiredParam).toBe('test'); + expect(fullParseResult.data.optionalParam).toBe('custom value'); + } + }); + it('should handle nested parameters correctly', () => { node.parameters = { topLevel: "={{ $fromAI('topLevel', 'Top level parameter', 'string') }}", diff --git a/packages/workflow/src/from-ai-parse-utils.ts b/packages/workflow/src/from-ai-parse-utils.ts index 8ad82f9e09..9131af6457 100644 --- a/packages/workflow/src/from-ai-parse-utils.ts +++ b/packages/workflow/src/from-ai-parse-utils.ts @@ -104,19 +104,32 @@ export function generateZodSchema(placeholder: FromAIArgument): z.ZodTypeAny { return schema; } +function isFromAIArgumentType(value: string): value is FromAIArgumentType { + return ['string', 'number', 'boolean', 'json'].includes(value.toLowerCase()); +} + /** * Parses the default value, preserving its original type. * @param value The default value as a string. + * @param type The expected type of the default value. * @returns The parsed default value in its appropriate type. */ function parseDefaultValue( value: string | undefined, + type: FromAIArgumentType = 'string', ): string | number | boolean | Record | undefined { - if (value === undefined || value === '') return undefined; + if (value === undefined) return value; + const lowerValue = value.toLowerCase(); - if (lowerValue === 'true') return true; - if (lowerValue === 'false') return false; - if (!isNaN(Number(value))) return Number(value); + if (type === 'string') { + return value.toString(); + } + + if (type === 'boolean' && (lowerValue === 'true' || lowerValue === 'false')) + return lowerValue === 'true'; + if (type === 'number' && !isNaN(Number(value))) return Number(value); + + // For type 'json' or any other case, attempt to parse as JSON try { return jsonParse(value); } catch { @@ -197,17 +210,17 @@ function parseArguments(argsString: string): FromAIArgument { return trimmed; }); - const type = cleanArgs?.[2] || 'string'; + const type = cleanArgs?.[2] ?? 'string'; - if (!['string', 'number', 'boolean', 'json'].includes(type.toLowerCase())) { + if (!isFromAIArgumentType(type)) { throw new ParseError(`Invalid type: ${type}`); } return { key: cleanArgs[0] || '', description: cleanArgs[1], - type: (cleanArgs?.[2] ?? 'string') as FromAIArgumentType, - defaultValue: parseDefaultValue(cleanArgs[3]), + type, + defaultValue: parseDefaultValue(cleanArgs[3], type), }; } diff --git a/packages/workflow/test/from-ai-parse-utils.test.ts b/packages/workflow/test/from-ai-parse-utils.test.ts index fd5fdbf7f3..734bc18969 100644 --- a/packages/workflow/test/from-ai-parse-utils.test.ts +++ b/packages/workflow/test/from-ai-parse-utils.test.ts @@ -11,10 +11,19 @@ describe('extractFromAICalls', () => { test.each<[string, [unknown, unknown, unknown, unknown]]>([ ['$fromAI("a", "b", "string")', ['a', 'b', 'string', undefined]], ['$fromAI("a", "b", "number", 5)', ['a', 'b', 'number', 5]], + ['$fromAI("a", "b", "number", "5")', ['a', 'b', 'number', 5]], ['$fromAI("a", "`", "number", 5)', ['a', '`', 'number', 5]], ['$fromAI("a", "\\`", "number", 5)', ['a', '`', 'number', 5]], // this is a bit surprising, but intended ['$fromAI("a", "\\n", "number", 5)', ['a', 'n', 'number', 5]], // this is a bit surprising, but intended ['{{ $fromAI("a", "b", "boolean") }}', ['a', 'b', 'boolean', undefined]], + ['{{ $fromAI("a", "b", "boolean", "true") }}', ['a', 'b', 'boolean', true]], + ['{{ $fromAI("a", "b", "boolean", "false") }}', ['a', 'b', 'boolean', false]], + ['{{ $fromAI("a", "b", "boolean", true) }}', ['a', 'b', 'boolean', true]], + ['{{ $fromAI("a", "b", "string", "") }}', ['a', 'b', 'string', '']], + ['{{ $fromAI("a", "b", "string", "null") }}', ['a', 'b', 'string', 'null']], + ['{{ $fromAI("a", "b", "string", "5") }}', ['a', 'b', 'string', '5']], + ['{{ $fromAI("a", "b", "string", "true") }}', ['a', 'b', 'string', 'true']], + ['{{ $fromAI("a", "b", "string", "{}") }}', ['a', 'b', 'string', '{}']], ])('should parse args as expected for %s', (formula, [key, description, type, defaultValue]) => { expect(extractFromAICalls(formula)).toEqual([ {