From 1f3f7a71943d9703d990f818d0201bdc5d19c0f6 Mon Sep 17 00:00:00 2001 From: Siddharth Chauhan <43745848+aibysid@users.noreply.github.com> Date: Wed, 26 Nov 2025 01:22:49 +0530 Subject: [PATCH] feat: Add structured JSON output support to Agent Node (#5470) * feat: Add structured JSON output support to Agent Node - Add agentStructuredOutput input parameter matching LLM Node structure - Implement configureStructuredOutput method to convert schema to Zod - Add createZodSchemaFromJSON helper for complex JSON schemas - Configure structured output before binding tools (required order) - Disable streaming when structured output is enabled - Extract structured fields in prepareOutputObject method - Resolves issue #5256 * lint fix * add structured output to Agent node * add structured output to Agent node --------- Co-authored-by: Henry --- .../components/nodes/agentflow/Agent/Agent.ts | 214 ++++++++++++++++-- .../components/nodes/agentflow/LLM/LLM.ts | 161 +------------ packages/components/src/utils.ts | 158 +++++++++++++ .../ui-component/input/suggestionOption.js | 2 +- 4 files changed, 355 insertions(+), 180 deletions(-) diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 1ad337c07..b8aa80222 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -28,7 +28,13 @@ import { replaceBase64ImagesWithFileReferences, updateFlowState } from '../utils' -import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils' +import { + convertMultiOptionsToStringArray, + getCredentialData, + getCredentialParam, + processTemplateVariables, + configureStructuredOutput +} from '../../../src/utils' import { addSingleFileToStorage } from '../../../src/storageUtils' import fetch from 'node-fetch' @@ -394,6 +400,108 @@ class Agent_Agentflow implements INode { ], default: 'userMessage' }, + { + label: 'JSON Structured Output', + name: 'agentStructuredOutput', + description: 'Instruct the Agent to give output in a JSON structured schema', + type: 'array', + optional: true, + acceptVariable: true, + array: [ + { + label: 'Key', + name: 'key', + type: 'string' + }, + { + label: 'Type', + name: 'type', + type: 'options', + options: [ + { + label: 'String', + name: 'string' + }, + { + label: 'String Array', + name: 'stringArray' + }, + { + label: 'Number', + name: 'number' + }, + { + label: 'Boolean', + name: 'boolean' + }, + { + label: 'Enum', + name: 'enum' + }, + { + label: 'JSON Array', + name: 'jsonArray' + } + ] + }, + { + label: 'Enum Values', + name: 'enumValues', + type: 'string', + placeholder: 'value1, value2, value3', + description: 'Enum values. Separated by comma', + optional: true, + show: { + 'agentStructuredOutput[$index].type': 'enum' + } + }, + { + label: 'JSON Schema', + name: 'jsonSchema', + type: 'code', + placeholder: `{ + "answer": { + "type": "string", + "description": "Value of the answer" + }, + "reason": { + "type": "string", + "description": "Reason for the answer" + }, + "optional": { + "type": "boolean" + }, + "count": { + "type": "number" + }, + "children": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Value of the children's answer" + } + } + } + } +}`, + description: 'JSON schema for the structured output', + optional: true, + hideCodeExecute: true, + show: { + 'agentStructuredOutput[$index].type': 'jsonArray' + } + }, + { + label: 'Description', + name: 'description', + type: 'string', + placeholder: 'Description of the key' + } + ] + }, { label: 'Update Flow State', name: 'agentUpdateState', @@ -770,6 +878,7 @@ class Agent_Agentflow implements INode { const memoryType = nodeData.inputs?.agentMemoryType as string const userMessage = nodeData.inputs?.agentUserMessage as string const _agentUpdateState = nodeData.inputs?.agentUpdateState + const _agentStructuredOutput = nodeData.inputs?.agentStructuredOutput const agentMessages = (nodeData.inputs?.agentMessages as unknown as ILLMMessage[]) ?? [] // Extract runtime state and history @@ -795,6 +904,8 @@ class Agent_Agentflow implements INode { const llmWithoutToolsBind = (await newLLMNodeInstance.init(newNodeData, '', options)) as BaseChatModel let llmNodeInstance = llmWithoutToolsBind + const isStructuredOutput = _agentStructuredOutput && Array.isArray(_agentStructuredOutput) && _agentStructuredOutput.length > 0 + const agentToolsBuiltInOpenAI = convertMultiOptionsToStringArray(nodeData.inputs?.agentToolsBuiltInOpenAI) if (agentToolsBuiltInOpenAI && agentToolsBuiltInOpenAI.length > 0) { for (const tool of agentToolsBuiltInOpenAI) { @@ -953,7 +1064,7 @@ class Agent_Agentflow implements INode { // Initialize response and determine if streaming is possible let response: AIMessageChunk = new AIMessageChunk('') const isLastNode = options.isLastNode as boolean - const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false + const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false && !isStructuredOutput // Start analytics if (analyticHandlers && options.parentTraceIds) { @@ -1002,7 +1113,8 @@ class Agent_Agentflow implements INode { llmWithoutToolsBind, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput }) response = result.response @@ -1031,7 +1143,14 @@ class Agent_Agentflow implements INode { } } else { if (isStreamable) { - response = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController) + response = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput + ) } else { response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) } @@ -1053,7 +1172,8 @@ class Agent_Agentflow implements INode { llmNodeInstance, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput }) response = result.response @@ -1080,8 +1200,9 @@ class Agent_Agentflow implements INode { sseStreamer.streamArtifactsEvent(chatId, flatten(artifacts)) } } - } else if (!humanInput && !isStreamable && isLastNode && sseStreamer) { + } else if (!humanInput && !isStreamable && isLastNode && sseStreamer && !isStructuredOutput) { // Stream whole response back to UI if not streaming and no tool calls + // Skip this if structured output is enabled - it will be streamed after conversion let finalResponse = '' if (response.content && Array.isArray(response.content)) { finalResponse = response.content.map((item: any) => item.text).join('\n') @@ -1159,6 +1280,23 @@ class Agent_Agentflow implements INode { finalResponse = await this.processSandboxLinks(finalResponse, options.baseURL, options.chatflowid, chatId) } + // If is structured output, then invoke LLM again with structured output at the very end after all tool calls + if (isStructuredOutput) { + llmNodeInstance = configureStructuredOutput(llmNodeInstance, _agentStructuredOutput) + const prompt = 'Convert the following response to the structured output format: ' + finalResponse + response = await llmNodeInstance.invoke(prompt, { signal: abortController?.signal }) + + if (typeof response === 'object') { + finalResponse = '```json\n' + JSON.stringify(response, null, 2) + '\n```' + } else { + finalResponse = response + } + + if (isLastNode && sseStreamer) { + sseStreamer.streamTokenEvent(chatId, finalResponse) + } + } + const output = this.prepareOutputObject( response, availableTools, @@ -1171,7 +1309,8 @@ class Agent_Agentflow implements INode { artifacts, additionalTokens, isWaitingForHumanInput, - fileAnnotations + fileAnnotations, + isStructuredOutput ) // End analytics tracking @@ -1561,13 +1700,14 @@ class Agent_Agentflow implements INode { llmNodeInstance: BaseChatModel, messages: BaseMessageLike[], chatId: string, - abortController: AbortController + abortController: AbortController, + isStructuredOutput: boolean = false ): Promise { let response = new AIMessageChunk('') try { for await (const chunk of await llmNodeInstance.stream(messages, { signal: abortController?.signal })) { - if (sseStreamer) { + if (sseStreamer && !isStructuredOutput) { let content = '' if (typeof chunk === 'string') { @@ -1610,7 +1750,8 @@ class Agent_Agentflow implements INode { artifacts: any[], additionalTokens: number = 0, isWaitingForHumanInput: boolean = false, - fileAnnotations: any[] = [] + fileAnnotations: any[] = [], + isStructuredOutput: boolean = false ): any { const output: any = { content: finalResponse, @@ -1645,6 +1786,15 @@ class Agent_Agentflow implements INode { output.responseMetadata = response.response_metadata } + if (isStructuredOutput && typeof response === 'object') { + const structuredOutput = response as Record + for (const key in structuredOutput) { + if (structuredOutput[key] !== undefined && structuredOutput[key] !== null) { + output[key] = structuredOutput[key] + } + } + } + // Add used tools, source documents and artifacts to output if (usedTools && usedTools.length > 0) { output.usedTools = flatten(usedTools) @@ -1710,7 +1860,8 @@ class Agent_Agentflow implements INode { llmNodeInstance, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput = false }: { response: AIMessageChunk messages: BaseMessageLike[] @@ -1724,6 +1875,7 @@ class Agent_Agentflow implements INode { isStreamable: boolean isLastNode: boolean iterationContext: ICommonObject + isStructuredOutput?: boolean }): Promise<{ response: AIMessageChunk usedTools: IUsedTool[] @@ -1803,7 +1955,9 @@ class Agent_Agentflow implements INode { const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```' const responseContent = response.content + `\nAttempting to use tool:\n${toolCallDetails}` response.content = responseContent - sseStreamer?.streamTokenEvent(chatId, responseContent) + if (!isStructuredOutput) { + sseStreamer?.streamTokenEvent(chatId, responseContent) + } return { response, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput: true } } @@ -1909,7 +2063,7 @@ class Agent_Agentflow implements INode { const lastToolOutput = usedTools[0]?.toolOutput || '' const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2) - if (sseStreamer) { + if (sseStreamer && !isStructuredOutput) { sseStreamer.streamTokenEvent(chatId, lastToolOutputString) } @@ -1938,12 +2092,19 @@ class Agent_Agentflow implements INode { let newResponse: AIMessageChunk if (isStreamable) { - newResponse = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController) + newResponse = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput + ) } else { newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) // Stream non-streaming response if this is the last node - if (isLastNode && sseStreamer) { + if (isLastNode && sseStreamer && !isStructuredOutput) { let responseContent = JSON.stringify(newResponse, null, 2) if (typeof newResponse.content === 'string') { responseContent = newResponse.content @@ -1978,7 +2139,8 @@ class Agent_Agentflow implements INode { llmNodeInstance, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput }) // Merge results from recursive tool calls @@ -2009,7 +2171,8 @@ class Agent_Agentflow implements INode { llmWithoutToolsBind, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput = false }: { humanInput: IHumanInput humanInputAction: Record | undefined @@ -2024,6 +2187,7 @@ class Agent_Agentflow implements INode { isStreamable: boolean isLastNode: boolean iterationContext: ICommonObject + isStructuredOutput?: boolean }): Promise<{ response: AIMessageChunk usedTools: IUsedTool[] @@ -2226,7 +2390,7 @@ class Agent_Agentflow implements INode { const lastToolOutput = usedTools[0]?.toolOutput || '' const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2) - if (sseStreamer) { + if (sseStreamer && !isStructuredOutput) { sseStreamer.streamTokenEvent(chatId, lastToolOutputString) } @@ -2257,12 +2421,19 @@ class Agent_Agentflow implements INode { } if (isStreamable) { - newResponse = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController) + newResponse = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput + ) } else { newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) // Stream non-streaming response if this is the last node - if (isLastNode && sseStreamer) { + if (isLastNode && sseStreamer && !isStructuredOutput) { let responseContent = JSON.stringify(newResponse, null, 2) if (typeof newResponse.content === 'string') { responseContent = newResponse.content @@ -2297,7 +2468,8 @@ class Agent_Agentflow implements INode { llmNodeInstance, isStreamable, isLastNode, - iterationContext + iterationContext, + isStructuredOutput }) // Merge results from recursive tool calls diff --git a/packages/components/nodes/agentflow/LLM/LLM.ts b/packages/components/nodes/agentflow/LLM/LLM.ts index c970466b4..a5bf4deb7 100644 --- a/packages/components/nodes/agentflow/LLM/LLM.ts +++ b/packages/components/nodes/agentflow/LLM/LLM.ts @@ -2,9 +2,8 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models' import { ICommonObject, IMessage, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface' import { AIMessageChunk, BaseMessageLike, MessageContentText } from '@langchain/core/messages' import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt' -import { z } from 'zod' import { AnalyticHandler } from '../../../src/handler' -import { ILLMMessage, IStructuredOutput } from '../Interface.Agentflow' +import { ILLMMessage } from '../Interface.Agentflow' import { getPastChatHistoryImageMessages, getUniqueImageMessages, @@ -12,7 +11,7 @@ import { replaceBase64ImagesWithFileReferences, updateFlowState } from '../utils' -import { processTemplateVariables } from '../../../src/utils' +import { processTemplateVariables, configureStructuredOutput } from '../../../src/utils' import { flatten } from 'lodash' class LLM_Agentflow implements INode { @@ -452,7 +451,7 @@ class LLM_Agentflow implements INode { // Configure structured output if specified const isStructuredOutput = _llmStructuredOutput && Array.isArray(_llmStructuredOutput) && _llmStructuredOutput.length > 0 if (isStructuredOutput) { - llmNodeInstance = this.configureStructuredOutput(llmNodeInstance, _llmStructuredOutput) + llmNodeInstance = configureStructuredOutput(llmNodeInstance, _llmStructuredOutput) } // Initialize response and determine if streaming is possible @@ -755,59 +754,6 @@ class LLM_Agentflow implements INode { } } - /** - * Configures structured output for the LLM - */ - private configureStructuredOutput(llmNodeInstance: BaseChatModel, llmStructuredOutput: IStructuredOutput[]): BaseChatModel { - try { - const zodObj: ICommonObject = {} - for (const sch of llmStructuredOutput) { - if (sch.type === 'string') { - zodObj[sch.key] = z.string().describe(sch.description || '') - } else if (sch.type === 'stringArray') { - zodObj[sch.key] = z.array(z.string()).describe(sch.description || '') - } else if (sch.type === 'number') { - zodObj[sch.key] = z.number().describe(sch.description || '') - } else if (sch.type === 'boolean') { - zodObj[sch.key] = z.boolean().describe(sch.description || '') - } else if (sch.type === 'enum') { - const enumValues = sch.enumValues?.split(',').map((item: string) => item.trim()) || [] - zodObj[sch.key] = z - .enum(enumValues.length ? (enumValues as [string, ...string[]]) : ['default']) - .describe(sch.description || '') - } else if (sch.type === 'jsonArray') { - const jsonSchema = sch.jsonSchema - if (jsonSchema) { - try { - // Parse the JSON schema - const schemaObj = JSON.parse(jsonSchema) - - // Create a Zod schema from the JSON schema - const itemSchema = this.createZodSchemaFromJSON(schemaObj) - - // Create an array schema of the item schema - zodObj[sch.key] = z.array(itemSchema).describe(sch.description || '') - } catch (err) { - console.error(`Error parsing JSON schema for ${sch.key}:`, err) - // Fallback to generic array of records - zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '') - } - } else { - // If no schema provided, use generic array of records - zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '') - } - } - } - const structuredOutput = z.object(zodObj) - - // @ts-ignore - return llmNodeInstance.withStructuredOutput(structuredOutput) - } catch (exception) { - console.error(exception) - return llmNodeInstance - } - } - /** * Handles streaming response from the LLM */ @@ -911,107 +857,6 @@ class LLM_Agentflow implements INode { sseStreamer.streamEndEvent(chatId) } - - /** - * Creates a Zod schema from a JSON schema object - * @param jsonSchema The JSON schema object - * @returns A Zod schema - */ - private createZodSchemaFromJSON(jsonSchema: any): z.ZodTypeAny { - // If the schema is an object with properties, create an object schema - if (typeof jsonSchema === 'object' && jsonSchema !== null) { - const schemaObj: Record = {} - - // Process each property in the schema - for (const [key, value] of Object.entries(jsonSchema)) { - if (value === null) { - // Handle null values - schemaObj[key] = z.null() - } else if (typeof value === 'object' && !Array.isArray(value)) { - // Check if the property has a type definition - if ('type' in value) { - const type = value.type as string - const description = ('description' in value ? (value.description as string) : '') || '' - - // Create the appropriate Zod type based on the type property - if (type === 'string') { - schemaObj[key] = z.string().describe(description) - } else if (type === 'number') { - schemaObj[key] = z.number().describe(description) - } else if (type === 'boolean') { - schemaObj[key] = z.boolean().describe(description) - } else if (type === 'array') { - // If it's an array type, check if items is defined - if ('items' in value && value.items) { - const itemSchema = this.createZodSchemaFromJSON(value.items) - schemaObj[key] = z.array(itemSchema).describe(description) - } else { - // Default to array of any if items not specified - schemaObj[key] = z.array(z.any()).describe(description) - } - } else if (type === 'object') { - // If it's an object type, check if properties is defined - if ('properties' in value && value.properties) { - const nestedSchema = this.createZodSchemaFromJSON(value.properties) - schemaObj[key] = nestedSchema.describe(description) - } else { - // Default to record of any if properties not specified - schemaObj[key] = z.record(z.any()).describe(description) - } - } else { - // Default to any for unknown types - schemaObj[key] = z.any().describe(description) - } - - // Check if the property is optional - if ('optional' in value && value.optional === true) { - schemaObj[key] = schemaObj[key].optional() - } - } else if (Array.isArray(value)) { - // Array values without a type property - if (value.length > 0) { - // If the array has items, recursively create a schema for the first item - const itemSchema = this.createZodSchemaFromJSON(value[0]) - schemaObj[key] = z.array(itemSchema) - } else { - // Empty array, allow any array - schemaObj[key] = z.array(z.any()) - } - } else { - // It's a nested object without a type property, recursively create schema - schemaObj[key] = this.createZodSchemaFromJSON(value) - } - } else if (Array.isArray(value)) { - // Array values - if (value.length > 0) { - // If the array has items, recursively create a schema for the first item - const itemSchema = this.createZodSchemaFromJSON(value[0]) - schemaObj[key] = z.array(itemSchema) - } else { - // Empty array, allow any array - schemaObj[key] = z.array(z.any()) - } - } else { - // For primitive values (which shouldn't be in the schema directly) - // Use the corresponding Zod type - if (typeof value === 'string') { - schemaObj[key] = z.string() - } else if (typeof value === 'number') { - schemaObj[key] = z.number() - } else if (typeof value === 'boolean') { - schemaObj[key] = z.boolean() - } else { - schemaObj[key] = z.any() - } - } - } - - return z.object(schemaObj) - } - - // Fallback to any for unknown types - return z.any() - } } module.exports = { nodeClass: LLM_Agentflow } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index fef9adac4..91fc75454 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -8,6 +8,7 @@ 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 { BaseChatModel } from '@langchain/core/language_models/chat_models' import { AES, enc } from 'crypto-js' import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages' import { Document } from '@langchain/core/documents' @@ -1941,3 +1942,160 @@ export async function parseWithTypeConversion(schema: T, throw e } } + +/** + * Configures structured output for the LLM using Zod schema + * @param {BaseChatModel} llmNodeInstance - The LLM instance to configure + * @param {any[]} structuredOutput - Array of structured output schema definitions + * @returns {BaseChatModel} - The configured LLM instance + */ +export const configureStructuredOutput = (llmNodeInstance: BaseChatModel, structuredOutput: any[]): BaseChatModel => { + try { + const zodObj: ICommonObject = {} + for (const sch of structuredOutput) { + if (sch.type === 'string') { + zodObj[sch.key] = z.string().describe(sch.description || '') + } else if (sch.type === 'stringArray') { + zodObj[sch.key] = z.array(z.string()).describe(sch.description || '') + } else if (sch.type === 'number') { + zodObj[sch.key] = z.number().describe(sch.description || '') + } else if (sch.type === 'boolean') { + zodObj[sch.key] = z.boolean().describe(sch.description || '') + } else if (sch.type === 'enum') { + const enumValues = sch.enumValues?.split(',').map((item: string) => item.trim()) || [] + zodObj[sch.key] = z + .enum(enumValues.length ? (enumValues as [string, ...string[]]) : ['default']) + .describe(sch.description || '') + } else if (sch.type === 'jsonArray') { + const jsonSchema = sch.jsonSchema + if (jsonSchema) { + try { + // Parse the JSON schema + const schemaObj = JSON.parse(jsonSchema) + + // Create a Zod schema from the JSON schema + const itemSchema = createZodSchemaFromJSON(schemaObj) + + // Create an array schema of the item schema + zodObj[sch.key] = z.array(itemSchema).describe(sch.description || '') + } catch (err) { + console.error(`Error parsing JSON schema for ${sch.key}:`, err) + // Fallback to generic array of records + zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '') + } + } else { + // If no schema provided, use generic array of records + zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '') + } + } + } + const structuredOutputSchema = z.object(zodObj) + + // @ts-ignore + return llmNodeInstance.withStructuredOutput(structuredOutputSchema) + } catch (exception) { + console.error(exception) + return llmNodeInstance + } +} + +/** + * Creates a Zod schema from a JSON schema object + * @param {any} jsonSchema - The JSON schema object + * @returns {z.ZodTypeAny} - A Zod schema + */ +export const createZodSchemaFromJSON = (jsonSchema: any): z.ZodTypeAny => { + // If the schema is an object with properties, create an object schema + if (typeof jsonSchema === 'object' && jsonSchema !== null) { + const schemaObj: Record = {} + + // Process each property in the schema + for (const [key, value] of Object.entries(jsonSchema)) { + if (value === null) { + // Handle null values + schemaObj[key] = z.null() + } else if (typeof value === 'object' && !Array.isArray(value)) { + // Check if the property has a type definition + if ('type' in value) { + const type = value.type as string + const description = ('description' in value ? (value.description as string) : '') || '' + + // Create the appropriate Zod type based on the type property + if (type === 'string') { + schemaObj[key] = z.string().describe(description) + } else if (type === 'number') { + schemaObj[key] = z.number().describe(description) + } else if (type === 'boolean') { + schemaObj[key] = z.boolean().describe(description) + } else if (type === 'array') { + // If it's an array type, check if items is defined + if ('items' in value && value.items) { + const itemSchema = createZodSchemaFromJSON(value.items) + schemaObj[key] = z.array(itemSchema).describe(description) + } else { + // Default to array of any if items not specified + schemaObj[key] = z.array(z.any()).describe(description) + } + } else if (type === 'object') { + // If it's an object type, check if properties is defined + if ('properties' in value && value.properties) { + const nestedSchema = createZodSchemaFromJSON(value.properties) + schemaObj[key] = nestedSchema.describe(description) + } else { + // Default to record of any if properties not specified + schemaObj[key] = z.record(z.any()).describe(description) + } + } else { + // Default to any for unknown types + schemaObj[key] = z.any().describe(description) + } + + // Check if the property is optional + if ('optional' in value && value.optional === true) { + schemaObj[key] = schemaObj[key].optional() + } + } else if (Array.isArray(value)) { + // Array values without a type property + if (value.length > 0) { + // If the array has items, recursively create a schema for the first item + const itemSchema = createZodSchemaFromJSON(value[0]) + schemaObj[key] = z.array(itemSchema) + } else { + // Empty array, allow any array + schemaObj[key] = z.array(z.any()) + } + } else { + // It's a nested object without a type property, recursively create schema + schemaObj[key] = createZodSchemaFromJSON(value) + } + } else if (Array.isArray(value)) { + // Array values + if (value.length > 0) { + // If the array has items, recursively create a schema for the first item + const itemSchema = createZodSchemaFromJSON(value[0]) + schemaObj[key] = z.array(itemSchema) + } else { + // Empty array, allow any array + schemaObj[key] = z.array(z.any()) + } + } else { + // For primitive values (which shouldn't be in the schema directly) + // Use the corresponding Zod type + if (typeof value === 'string') { + schemaObj[key] = z.string() + } else if (typeof value === 'number') { + schemaObj[key] = z.number() + } else if (typeof value === 'boolean') { + schemaObj[key] = z.boolean() + } else { + schemaObj[key] = z.any() + } + } + } + + return z.object(schemaObj) + } + + // Fallback to any for unknown types + return z.any() +} diff --git a/packages/ui/src/ui-component/input/suggestionOption.js b/packages/ui/src/ui-component/input/suggestionOption.js index 6afbf95f8..0247c8a05 100644 --- a/packages/ui/src/ui-component/input/suggestionOption.js +++ b/packages/ui/src/ui-component/input/suggestionOption.js @@ -112,7 +112,7 @@ export const suggestionOptions = ( category: 'Node Outputs' }) - const structuredOutputs = nodeData?.inputs?.llmStructuredOutput ?? [] + const structuredOutputs = nodeData?.inputs?.llmStructuredOutput ?? nodeData?.inputs?.agentStructuredOutput ?? [] if (structuredOutputs && structuredOutputs.length > 0) { structuredOutputs.forEach((item) => { defaultItems.unshift({