Feature/agentflow v2 (#4298)

* agent flow v2

* chat message background

* conditon agent flow

* add sticky note

* update human input dynamic prompt

* add HTTP node

* add default tool icon

* fix export duplicate agentflow v2

* add agentflow v2 marketplaces

* refractor memoization, add iteration nodes

* add agentflow v2 templates

* add agentflow generator

* add migration scripts for mysql, mariadb, posrgres and fix date filters for executions

* update agentflow chat history config

* fix get all flows error after deletion and rename

* add previous nodes from parent node

* update generator prompt

* update run time state when using iteration nodes

* prevent looping connection, prevent duplication of start node, add executeflow node, add nodes agentflow, chat history variable

* update embed

* convert form input to string

* bump openai version

* add react rewards

* add prompt generator to prediction queue

* add array schema to overrideconfig

* UI touchup

* update embedded chat version

* fix node info dialog

* update start node and loop default iteration

* update UI fixes for agentflow v2

* fix async drop down

* add export import to agentflowsv2, executions, fix UI bugs

* add default empty object to flowlisttable

* add ability to share trace link publicly, allow MCP tool use for Agent and Assistant

* add runtime message length to variable, display conditions on UI

* fix array validation

* add ability to add knowledge from vector store and embeddings for agent

* add agent tool require human input

* add ephemeral memory to start node

* update agent flow node to show vs and embeddings icons

* feat: add import chat data functionality for AgentFlowV2

* feat: set chatMessage.executionId to null if not found in import JSON file or database

* fix: MariaDB execution migration script to utf8mb4_unicode_520_ci

---------

Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com>
Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
Henry Heng
2025-05-10 10:21:26 +08:00
committed by GitHub
parent 82e6f43b5c
commit 7924fbce0d
216 changed files with 33304 additions and 5269 deletions
+51
View File
@@ -99,6 +99,16 @@ export class SSEStreamer implements IServerSideEventStreamer {
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamCalledToolsEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
const clientResponse = {
event: 'calledTools',
data: data
}
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamFileAnnotationsEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
@@ -139,6 +149,36 @@ export class SSEStreamer implements IServerSideEventStreamer {
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamAgentFlowEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
const clientResponse = {
event: 'agentFlowEvent',
data: data
}
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamAgentFlowExecutedDataEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
const clientResponse = {
event: 'agentFlowExecutedData',
data: data
}
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamNextAgentFlowEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
const clientResponse = {
event: 'nextAgentFlow',
data: data
}
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
streamActionEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
@@ -206,4 +246,15 @@ export class SSEStreamer implements IServerSideEventStreamer {
this.streamCustomEvent(chatId, 'metadata', metadataJson)
}
}
streamUsageMetadataEvent(chatId: string, data: any): void {
const client = this.clients[chatId]
if (client) {
const clientResponse = {
event: 'usageMetadata',
data: data
}
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
}
}
}
File diff suppressed because it is too large Load Diff
+30 -6
View File
@@ -63,6 +63,7 @@ import { buildAgentGraph } from './buildAgentGraph'
import { getErrorMessage } from '../errors/utils'
import { FLOWISE_METRIC_COUNTERS, FLOWISE_COUNTER_STATUS, IMetricsProvider } from '../Interface.Metrics'
import { OMIT_QUEUE_JOB_DATA } from './constants'
import { executeAgentFlow } from './buildAgentflow'
/*
* Initialize the ending node to be executed
@@ -236,7 +237,8 @@ export const executeFlow = async ({
baseURL,
isInternal,
files,
signal
signal,
isTool
}: IExecuteFlowParams) => {
// Ensure incomingInput has all required properties with default values
incomingInput = {
@@ -260,8 +262,8 @@ export const executeFlow = async ({
*/
let fileUploads: IFileUpload[] = []
let uploadedFilesContent = ''
if (incomingInput.uploads) {
fileUploads = incomingInput.uploads
if (uploads) {
fileUploads = uploads
for (let i = 0; i < fileUploads.length; i += 1) {
const upload = fileUploads[i]
@@ -373,6 +375,26 @@ export const executeFlow = async ({
}
}
const isAgentFlowV2 = chatflow.type === 'AGENTFLOW'
if (isAgentFlowV2) {
return executeAgentFlow({
componentNodes,
incomingInput,
chatflow,
chatId,
appDataSource,
telemetry,
cachePool,
sseStreamer,
baseURL,
isInternal,
uploadedFilesContent,
fileUploads,
signal,
isTool
})
}
/*** Get chatflows and prepare data ***/
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
@@ -498,7 +520,7 @@ export const executeFlow = async ({
memoryType,
sessionId,
createdDate: userMessageDateTime,
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined,
fileUploads: uploads ? JSON.stringify(fileUploads) : undefined,
leadEmail: incomingInput.leadEmail
}
await utilAddChatMessage(userMessage, appDataSource)
@@ -819,12 +841,14 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
}
const isAgentFlow = chatflow.type === 'MULTIAGENT'
const httpProtocol = req.get('x-forwarded-proto') || req.protocol
const baseURL = `${httpProtocol}://${req.get('host')}`
const incomingInput: IncomingInput = req.body || {} // Ensure incomingInput is never undefined
const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4()
const files = (req.files as Express.Multer.File[]) || []
const abortControllerId = `${chatflow.id}_${chatId}`
const isTool = req.get('flowise-tool') === 'true'
try {
// Validate API Key if its external API request
@@ -846,7 +870,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
sseStreamer: appServer.sseStreamer,
telemetry: appServer.telemetry,
cachePool: appServer.cachePool,
componentNodes: appServer.nodesPool.componentNodes
componentNodes: appServer.nodesPool.componentNodes,
isTool // used to disable streaming if incoming request its from ChatflowTool
}
if (process.env.MODE === MODE.QUEUE) {
@@ -868,7 +893,6 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
const signal = new AbortController()
appServer.abortControllerPool.add(abortControllerId, signal)
executeData.signal = signal
const result = await executeFlow(executeData)
appServer.abortControllerPool.remove(abortControllerId)
+2
View File
@@ -3,6 +3,7 @@ export const WHITELIST_URLS = [
'/api/v1/chatflows/apikey/',
'/api/v1/public-chatflows',
'/api/v1/public-chatbotConfig',
'/api/v1/public-executions',
'/api/v1/prediction/',
'/api/v1/vector/upsert/',
'/api/v1/node-icon/',
@@ -28,6 +29,7 @@ export const INPUT_PARAMS_TYPE = [
'asyncMultiOptions',
'options',
'multiOptions',
'array',
'datagrid',
'string',
'number',
@@ -52,6 +52,7 @@ export const utilGetChatMessage = async ({
// do the join with chat message feedback based on messageId for each chat message in the chatflow
query
.leftJoinAndSelect('chat_message.execution', 'execution')
.leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
.where('chat_message.chatflowid = :chatflowid', { chatflowid })
@@ -121,6 +122,9 @@ export const utilGetChatMessage = async ({
createdDate: createdDateQuery,
id: messageId ?? undefined
},
relations: {
execution: true
},
order: {
createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
}
+35 -14
View File
@@ -93,22 +93,43 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
'seqStart'
]
if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name))) {
nodes.forEach((node: IReactFlowNode) => {
const data = node.data
if (data.category === 'Chat Models' && data.inputs?.['allowImageUploads'] === true) {
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
node.data.inputParams.map((param: INodeParams) => {
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
imgUploadSizeAndTypes.push({
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
maxUploadSize: 5
})
isImageUploadAllowed = true
}
})
const isAgentflow = nodes.some((node) => node.data.category === 'Agent Flows')
if (isAgentflow) {
// check through all the nodes and check if any of the nodes data inputs agentModelConfig or llmModelConfig or conditionAgentModelConfig has allowImageUploads
nodes.forEach((node) => {
if (node.data.category === 'Agent Flows') {
if (
node.data.inputs?.agentModelConfig?.allowImageUploads ||
node.data.inputs?.llmModelConfig?.allowImageUploads ||
node.data.inputs?.conditionAgentModelConfig?.allowImageUploads
) {
imgUploadSizeAndTypes.push({
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
maxUploadSize: 5
})
isImageUploadAllowed = true
}
}
})
} else {
if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name))) {
nodes.forEach((node: IReactFlowNode) => {
const data = node.data
if (data.category === 'Chat Models' && data.inputs?.['allowImageUploads'] === true) {
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
node.data.inputParams.map((param: INodeParams) => {
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
imgUploadSizeAndTypes.push({
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
maxUploadSize: 5
})
isImageUploadAllowed = true
}
})
}
})
}
}
return {
+121 -9
View File
@@ -64,10 +64,11 @@ import {
SecretsManagerClientConfig
} from '@aws-sdk/client-secrets-manager'
const QUESTION_VAR_PREFIX = 'question'
const FILE_ATTACHMENT_PREFIX = 'file_attachment'
const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
export const QUESTION_VAR_PREFIX = 'question'
export const FILE_ATTACHMENT_PREFIX = 'file_attachment'
export const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
export const RUNTIME_MESSAGES_LENGTH_VAR_PREFIX = 'runtime_messages_length'
export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
let secretsManagerClient: SecretsManagerClient | null = null
const USE_AWS_SECRETS_MANAGER = process.env.SECRETKEY_STORAGE_TYPE === 'aws'
@@ -238,6 +239,22 @@ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) =
return { startingNodeIds, depthQueue: depthQueueReversed }
}
/**
* Get starting node and check if flow is valid
* @param {INodeDependencies} nodeDependencies
*/
export const getStartingNode = (nodeDependencies: INodeDependencies) => {
// Find starting node
const startingNodeIds = [] as string[]
Object.keys(nodeDependencies).forEach((nodeId) => {
if (nodeDependencies[nodeId] === 0) {
startingNodeIds.push(nodeId)
}
})
return { startingNodeIds }
}
/**
* Get all connected nodes from startnode
* @param {INodeDependencies} graph
@@ -763,7 +780,7 @@ export const clearSessionMemory = async (
}
}
const getGlobalVariable = async (
export const getGlobalVariable = async (
overrideConfig?: ICommonObject,
availableVariables: IVariable[] = [],
variableOverrides: ICommonObject[] = []
@@ -990,7 +1007,6 @@ export const resolveVariables = async (
variableOverrides: ICommonObject[] = []
): Promise<INodeData> => {
let flowNodeData = cloneDeep(reactFlowNodeData)
const types = 'inputs'
const getParamValues = async (paramsObj: ICommonObject) => {
for (const key in paramsObj) {
@@ -1030,7 +1046,7 @@ export const resolveVariables = async (
}
}
const paramsObj = flowNodeData[types] ?? {}
const paramsObj = flowNodeData['inputs'] ?? {}
await getParamValues(paramsObj)
return flowNodeData
@@ -1244,7 +1260,8 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
for (const flowNode of reactFlowNodes) {
for (const inputParam of flowNode.data.inputParams) {
let obj: IOverrideConfig
let obj: IOverrideConfig | undefined
if (inputParam.type === 'file') {
obj = {
node: flowNode.data.label,
@@ -1285,6 +1302,34 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
}
}
continue
} else if (inputParam.type === 'array') {
// get array item schema
const arrayItem = inputParam.array
if (Array.isArray(arrayItem)) {
const arraySchema = []
// Each array item is a field definition
for (const item of arrayItem) {
let itemType = item.type
if (itemType === 'options') {
const availableOptions = item.options?.map((option) => option.name).join(', ')
itemType = `(${availableOptions})`
} else if (itemType === 'file') {
itemType = item.fileType ?? item.type
}
arraySchema.push({
name: item.name,
type: itemType
})
}
obj = {
node: flowNode.data.label,
nodeId: flowNode.data.id,
label: inputParam.label,
name: inputParam.name,
type: inputParam.type,
schema: arraySchema
}
}
} else {
obj = {
node: flowNode.data.label,
@@ -1294,7 +1339,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
type: inputParam.type === 'password' ? 'string' : inputParam.type
}
}
if (!configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) {
if (obj && !configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) {
configs.push(obj)
}
}
@@ -1814,3 +1859,70 @@ export const getMulterStorage = () => {
return multer({ dest: getUploadPath() })
}
}
/**
* Calculate depth of each node from starting nodes
* @param {INodeDirectedGraph} graph
* @param {string[]} startingNodeIds
* @returns {Record<string, number>} Map of nodeId to its depth
*/
export const calculateNodesDepth = (graph: INodeDirectedGraph, startingNodeIds: string[]): Record<string, number> => {
const depths: Record<string, number> = {}
const visited = new Set<string>()
// Initialize all nodes with depth -1 (unvisited)
for (const nodeId in graph) {
depths[nodeId] = -1
}
// BFS queue with [nodeId, depth]
const queue: [string, number][] = startingNodeIds.map((id) => [id, 0])
// Set starting nodes depth to 0
startingNodeIds.forEach((id) => {
depths[id] = 0
})
while (queue.length > 0) {
const [currentNode, currentDepth] = queue.shift()!
if (visited.has(currentNode)) continue
visited.add(currentNode)
// Process all neighbors
for (const neighbor of graph[currentNode]) {
if (!visited.has(neighbor)) {
// Update depth if unvisited or found shorter path
if (depths[neighbor] === -1 || depths[neighbor] > currentDepth + 1) {
depths[neighbor] = currentDepth + 1
}
queue.push([neighbor, currentDepth + 1])
}
}
}
return depths
}
/**
* Helper function to get all nodes in a path starting from a node
* @param {INodeDirectedGraph} graph
* @param {string} startNode
* @returns {string[]}
*/
export const getAllNodesInPath = (startNode: string, graph: INodeDirectedGraph): string[] => {
const nodes = new Set<string>()
const queue = [startNode]
while (queue.length > 0) {
const current = queue.shift()!
if (nodes.has(current)) continue
nodes.add(current)
if (graph[current]) {
queue.push(...graph[current])
}
}
return Array.from(nodes)
}