mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 13:00:56 +03:00
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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user