Bugfix/Array Input Variables (#5196)

- Replace manual template variable processing in multiple components with a new utility function `processTemplateVariables`.
This commit is contained in:
Henry Heng
2025-09-12 14:42:34 +01:00
committed by GitHub
parent 736c2b11a1
commit 4987a2880d
9 changed files with 199 additions and 81 deletions
@@ -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(
@@ -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,
@@ -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 = []
+2 -31
View File
@@ -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(
@@ -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,
@@ -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,
+2 -2
View File
@@ -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<string, any> = {}
for (const state of llmUpdateState) {
for (const state of updateState) {
newFlowState[state.key] = state.value
}
+48 -1
View File
@@ -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
}