diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 781028ac..39759f0e 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -28,7 +28,7 @@ import { replaceBase64ImagesWithFileReferences, updateFlowState } from '../utils' -import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils' import { addSingleFileToStorage } from '../../../src/storageUtils' import fetch from 'node-fetch' @@ -1086,13 +1086,7 @@ class Agent_Agentflow implements INode { } // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - if (newState[key].toString().includes('{{ output }}')) { - newState[key] = newState[key].replaceAll('{{ output }}', finalResponse) - } - } - } + newState = processTemplateVariables(newState, finalResponse) // Replace the actual messages array with one that includes the file references for images instead of base64 data const messagesWithFileReferences = replaceBase64ImagesWithFileReferences( diff --git a/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts b/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts index 3bdc6068..fbb1097f 100644 --- a/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts @@ -8,7 +8,7 @@ import { INodeParams, IServerSideEventStreamer } from '../../../src/Interface' -import { getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { getVars, executeJavaScriptCode, createCodeExecutionSandbox, processTemplateVariables } from '../../../src/utils' import { updateFlowState } from '../utils' interface ICustomFunctionInputVariables { @@ -145,19 +145,13 @@ class CustomFunction_Agentflow implements INode { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity - // Update flow state if needed - let newState = { ...state } - if (_customFunctionUpdateState && Array.isArray(_customFunctionUpdateState) && _customFunctionUpdateState.length > 0) { - newState = updateFlowState(state, _customFunctionUpdateState) - } - const variables = await getVars(appDataSource, databaseEntities, nodeData, options) const flow = { chatflowId: options.chatflowid, sessionId: options.sessionId, chatId: options.chatId, input, - state: newState + state } // Create additional sandbox variables for custom function inputs @@ -190,15 +184,14 @@ class CustomFunction_Agentflow implements INode { finalOutput = JSON.stringify(response, null, 2) } - // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - if (newState[key].toString().includes('{{ output }}')) { - newState[key] = newState[key].replaceAll('{{ output }}', finalOutput) - } - } + // Update flow state if needed + let newState = { ...state } + if (_customFunctionUpdateState && Array.isArray(_customFunctionUpdateState) && _customFunctionUpdateState.length > 0) { + newState = updateFlowState(state, _customFunctionUpdateState) } + newState = processTemplateVariables(newState, finalOutput) + const returnOutput = { id: nodeData.id, name: this.name, diff --git a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts index 492c00af..195be510 100644 --- a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts +++ b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts @@ -8,7 +8,7 @@ import { IServerSideEventStreamer } from '../../../src/Interface' import axios, { AxiosRequestConfig } from 'axios' -import { getCredentialData, getCredentialParam } from '../../../src/utils' +import { getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils' import { DataSource } from 'typeorm' import { BaseMessageLike } from '@langchain/core/messages' import { updateFlowState } from '../utils' @@ -222,13 +222,7 @@ class ExecuteFlow_Agentflow implements INode { } // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - if (newState[key].toString().includes('{{ output }}')) { - newState[key] = newState[key].replaceAll('{{ output }}', resultText) - } - } - } + newState = processTemplateVariables(newState, resultText) // Only add to runtime chat history if this is the first node const inputMessages = [] diff --git a/packages/components/nodes/agentflow/LLM/LLM.ts b/packages/components/nodes/agentflow/LLM/LLM.ts index afafc4a5..8ad1d2aa 100644 --- a/packages/components/nodes/agentflow/LLM/LLM.ts +++ b/packages/components/nodes/agentflow/LLM/LLM.ts @@ -12,7 +12,7 @@ import { replaceBase64ImagesWithFileReferences, updateFlowState } from '../utils' -import { get } from 'lodash' +import { processTemplateVariables } from '../../../src/utils' class LLM_Agentflow implements INode { label: string @@ -529,36 +529,7 @@ class LLM_Agentflow implements INode { } // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - const stateValue = newState[key].toString() - if (stateValue.includes('{{ output')) { - // Handle simple output replacement - if (stateValue === '{{ output }}') { - newState[key] = finalResponse - continue - } - - // Handle JSON path expressions like {{ output.item1 }} - // eslint-disable-next-line - const match = stateValue.match(/{{[\s]*output\.([\w\.]+)[\s]*}}/) - if (match) { - try { - // Parse the response if it's JSON - const jsonResponse = typeof finalResponse === 'string' ? JSON.parse(finalResponse) : finalResponse - // Get the value using lodash get - const path = match[1] - const value = get(jsonResponse, path) - newState[key] = value ?? stateValue // Fall back to original if path not found - } catch (e) { - // If JSON parsing fails, keep original template - console.warn(`Failed to parse JSON or find path in output: ${e}`) - newState[key] = stateValue - } - } - } - } - } + newState = processTemplateVariables(newState, finalResponse) // Replace the actual messages array with one that includes the file references for images instead of base64 data const messagesWithFileReferences = replaceBase64ImagesWithFileReferences( diff --git a/packages/components/nodes/agentflow/Retriever/Retriever.ts b/packages/components/nodes/agentflow/Retriever/Retriever.ts index b9af63b7..e7ce426c 100644 --- a/packages/components/nodes/agentflow/Retriever/Retriever.ts +++ b/packages/components/nodes/agentflow/Retriever/Retriever.ts @@ -8,6 +8,7 @@ import { IServerSideEventStreamer } from '../../../src/Interface' import { updateFlowState } from '../utils' +import { processTemplateVariables } from '../../../src/utils' import { DataSource } from 'typeorm' import { BaseRetriever } from '@langchain/core/retrievers' import { Document } from '@langchain/core/documents' @@ -197,14 +198,7 @@ class Retriever_Agentflow implements INode { sseStreamer.streamTokenEvent(chatId, finalOutput) } - // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - if (newState[key].toString().includes('{{ output }}')) { - newState[key] = newState[key].replaceAll('{{ output }}', finalOutput) - } - } - } + newState = processTemplateVariables(newState, finalOutput) const returnOutput = { id: nodeData.id, diff --git a/packages/components/nodes/agentflow/Tool/Tool.ts b/packages/components/nodes/agentflow/Tool/Tool.ts index 59b81f55..300aaafa 100644 --- a/packages/components/nodes/agentflow/Tool/Tool.ts +++ b/packages/components/nodes/agentflow/Tool/Tool.ts @@ -1,5 +1,6 @@ import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface' import { updateFlowState } from '../utils' +import { processTemplateVariables } from '../../../src/utils' import { Tool } from '@langchain/core/tools' import { ARTIFACTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' import zodToJsonSchema from 'zod-to-json-schema' @@ -330,14 +331,7 @@ class Tool_Agentflow implements INode { sseStreamer.streamTokenEvent(chatId, toolOutput) } - // Process template variables in state - if (newState && Object.keys(newState).length > 0) { - for (const key in newState) { - if (newState[key].toString().includes('{{ output }}')) { - newState[key] = newState[key].replaceAll('{{ output }}', toolOutput) - } - } - } + newState = processTemplateVariables(newState, toolOutput) const returnOutput = { id: nodeData.id, diff --git a/packages/components/nodes/agentflow/utils.ts b/packages/components/nodes/agentflow/utils.ts index 1701b7e4..14d832c8 100644 --- a/packages/components/nodes/agentflow/utils.ts +++ b/packages/components/nodes/agentflow/utils.ts @@ -459,9 +459,9 @@ export const getPastChatHistoryImageMessages = async ( /** * Updates the flow state with new values */ -export const updateFlowState = (state: ICommonObject, llmUpdateState: IFlowState[]): ICommonObject => { +export const updateFlowState = (state: ICommonObject, updateState: IFlowState[]): ICommonObject => { let newFlowState: Record = {} - for (const state of llmUpdateState) { + for (const state of updateState) { newFlowState[state.key] = state.value } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 3f3e86c6..01e60e82 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -8,7 +8,7 @@ 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 } from 'lodash' +import { omit, get } from 'lodash' import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages' import { Document } from '@langchain/core/documents' import { getFileFromStorage } from './storageUtils' @@ -1609,3 +1609,50 @@ export const createCodeExecutionSandbox = ( return sandbox } + +/** + * Process template variables in state object, replacing {{ output }} and {{ output.property }} patterns + * @param {ICommonObject} state - The state object to process + * @param {any} finalOutput - The output value to substitute + * @returns {ICommonObject} - The processed state object + */ +export const processTemplateVariables = (state: ICommonObject, finalOutput: any): ICommonObject => { + if (!state || Object.keys(state).length === 0) { + return state + } + + const newState = { ...state } + + for (const key in newState) { + const stateValue = newState[key].toString() + if (stateValue.includes('{{ output') || stateValue.includes('{{output')) { + // Handle simple output replacement (with or without spaces) + if (stateValue === '{{ output }}' || stateValue === '{{output}}') { + newState[key] = finalOutput + continue + } + + // Handle JSON path expressions like {{ output.updated }} or {{output.updated}} + // eslint-disable-next-line + const match = stateValue.match(/\{\{\s*output\.([\w\.]+)\s*\}\}/) + if (match) { + try { + // Parse the response if it's JSON + const jsonResponse = typeof finalOutput === 'string' ? JSON.parse(finalOutput) : finalOutput + // Get the value using lodash get + const path = match[1] + const value = get(jsonResponse, path) + newState[key] = value ?? stateValue // Fall back to original if path not found + } catch (e) { + // If JSON parsing fails, keep original template + newState[key] = stateValue + } + } else { + // Handle simple {{ output }} replacement for backward compatibility + newState[key] = newState[key].replaceAll('{{ output }}', finalOutput) + } + } + } + + return newState +} diff --git a/packages/server/src/utils/buildAgentflow.ts b/packages/server/src/utils/buildAgentflow.ts index deea4139..c165f259 100644 --- a/packages/server/src/utils/buildAgentflow.ts +++ b/packages/server/src/utils/buildAgentflow.ts @@ -216,6 +216,7 @@ export const resolveVariables = async ( variableOverrides: IVariableOverride[], uploadedFilesContent: string, chatHistory: IMessage[], + componentNodes: IComponentNodes, agentFlowExecutedData?: IAgentflowExecutedData[], iterationContext?: ICommonObject ): Promise => { @@ -390,6 +391,135 @@ export const resolveVariables = async ( } const getParamValues = async (paramsObj: ICommonObject) => { + /* + * EXAMPLE SCENARIO: + * + * 1. Agent node has inputParam: { name: "agentTools", type: "array", array: [{ name: "agentSelectedTool", loadConfig: true }] } + * 2. Inputs contain: { agentTools: [{ agentSelectedTool: "requestsGet", agentSelectedToolConfig: { requestsGetHeaders: "Bearer {{ $vars.TOKEN }}" } }] } + * 3. We need to resolve the variable in requestsGetHeaders because RequestsGet node defines requestsGetHeaders with acceptVariable: true + * + * STEP 1: Find all parameters with loadConfig=true (e.g., "agentSelectedTool") + * STEP 2: Find their values in inputs (e.g., "requestsGet") + * STEP 3: Look up component node definition for "requestsGet" + * STEP 4: Find which of its parameters have acceptVariable=true (e.g., "requestsGetHeaders") + * STEP 5: Find the config object (e.g., "agentSelectedToolConfig") + * STEP 6: Resolve variables in config parameters that accept variables + */ + + // Helper function to find params with loadConfig recursively + // Example: Finds ["agentModel", "agentSelectedTool"] from the inputParams structure + const findParamsWithLoadConfig = (inputParams: any[]): string[] => { + const paramsWithLoadConfig: string[] = [] + + for (const param of inputParams) { + // Direct loadConfig param (e.g., agentModel with loadConfig: true) + if (param.loadConfig === true) { + paramsWithLoadConfig.push(param.name) + } + + // Check nested array parameters (e.g., agentTools.array contains agentSelectedTool with loadConfig: true) + if (param.type === 'array' && param.array && Array.isArray(param.array)) { + const nestedParams = findParamsWithLoadConfig(param.array) + paramsWithLoadConfig.push(...nestedParams) + } + } + + return paramsWithLoadConfig + } + + // Helper function to find value of a parameter recursively in nested objects/arrays + // Example: Searches for "agentSelectedTool" value in complex nested inputs structure + // Returns "requestsGet" when found in agentTools[0].agentSelectedTool + const findParamValue = (obj: any, paramName: string): any => { + if (typeof obj !== 'object' || obj === null) { + return undefined + } + + // Handle arrays (e.g., agentTools array) + if (Array.isArray(obj)) { + for (const item of obj) { + const result = findParamValue(item, paramName) + if (result !== undefined) { + return result + } + } + return undefined + } + + // Direct property match + if (Object.prototype.hasOwnProperty.call(obj, paramName)) { + return obj[paramName] + } + + // Recursively search nested objects + for (const value of Object.values(obj)) { + const result = findParamValue(value, paramName) + if (result !== undefined) { + return result + } + } + + return undefined + } + + // Helper function to process config parameters with acceptVariable + // Example: Processes agentSelectedToolConfig object, resolving variables in requestsGetHeaders + const processConfigParams = async (configObj: any, configParamWithAcceptVariables: string[]) => { + if (typeof configObj !== 'object' || configObj === null) { + return + } + + for (const [key, value] of Object.entries(configObj)) { + // Only resolve variables for parameters that accept them + // Example: requestsGetHeaders is in configParamWithAcceptVariables, so resolve "Bearer {{ $vars.TOKEN }}" + if (configParamWithAcceptVariables.includes(key)) { + configObj[key] = await resolveNodeReference(value) + } + } + } + + // STEP 1: Get all params with loadConfig from inputParams + // Example result: ["agentModel", "agentSelectedTool"] + const paramsWithLoadConfig = findParamsWithLoadConfig(reactFlowNodeData.inputParams) + + // STEP 2-6: Process each param with loadConfig + for (const paramWithLoadConfig of paramsWithLoadConfig) { + // STEP 2: Find the value of this parameter in the inputs + // Example: paramWithLoadConfig="agentSelectedTool", paramValue="requestsGet" + const paramValue = findParamValue(paramsObj, paramWithLoadConfig) + + if (paramValue && componentNodes[paramValue]) { + // STEP 3: Get the node instance inputs to find params with acceptVariable + // Example: componentNodes["requestsGet"] contains the RequestsGet node definition + const nodeInstance = componentNodes[paramValue] + const configParamWithAcceptVariables: string[] = [] + + // STEP 4: Find which parameters of the component accept variables + // Example: RequestsGet has inputs like { name: "requestsGetHeaders", acceptVariable: true } + if (nodeInstance.inputs && Array.isArray(nodeInstance.inputs)) { + for (const input of nodeInstance.inputs) { + if (input.acceptVariable === true) { + configParamWithAcceptVariables.push(input.name) + } + } + } + // Example result: configParamWithAcceptVariables = ["requestsGetHeaders", "requestsGetUrl", ...] + + // STEP 5: Look for the config object (paramName + "Config") + // Example: Look for "agentSelectedToolConfig" in the inputs + const configParamName = paramWithLoadConfig + 'Config' + const configValue = findParamValue(paramsObj, configParamName) + + // STEP 6: Process config object to resolve variables + // Example: Resolve "Bearer {{ $vars.TOKEN }}" in requestsGetHeaders + if (configValue && configParamWithAcceptVariables.length > 0) { + await processConfigParams(configValue, configParamWithAcceptVariables) + } + } + } + + // Original logic for direct acceptVariable params (maintains backward compatibility) + // Example: Direct params like agentUserMessage with acceptVariable: true for (const key in paramsObj) { const paramValue = paramsObj[key] const isAcceptVariable = reactFlowNodeData.inputParams.find((param) => param.name === key)?.acceptVariable ?? false @@ -912,6 +1042,7 @@ const executeNode = async ({ variableOverrides, uploadedFilesContent, chatHistory, + componentNodes, agentFlowExecutedData, iterationContext )