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
+2
View File
@@ -78,6 +78,7 @@ export const exportData = (exportAllData) => {
try {
return {
AgentFlow: sanitizeChatflow(exportAllData.AgentFlow),
AgentFlowV2: sanitizeChatflow(exportAllData.AgentFlowV2),
AssistantFlow: sanitizeChatflow(exportAllData.AssistantFlow),
AssistantCustom: sanitizeAssistant(exportAllData.AssistantCustom),
AssistantOpenAI: sanitizeAssistant(exportAllData.AssistantOpenAI),
@@ -88,6 +89,7 @@ export const exportData = (exportAllData) => {
CustomTemplate: exportAllData.CustomTemplate,
DocumentStore: exportAllData.DocumentStore,
DocumentStoreFileChunk: exportAllData.DocumentStoreFileChunk,
Execution: exportAllData.Execution,
Tool: sanitizeTool(exportAllData.Tool),
Variable: sanitizeVariable(exportAllData.Variable)
}
+308 -65
View File
@@ -1,4 +1,4 @@
import { uniq } from 'lodash'
import { uniq, get, isEqual } from 'lodash'
import moment from 'moment'
export const getUniqueNodeId = (nodeData, nodes) => {
@@ -16,6 +16,94 @@ export const getUniqueNodeId = (nodeData, nodes) => {
return baseId
}
export const getUniqueNodeLabel = (nodeData, nodes) => {
if (nodeData.type === 'StickyNote') return nodeData.label
if (nodeData.name === 'startAgentflow') return nodeData.label
let suffix = 0
// Construct base ID
let baseId = `${nodeData.name}_${suffix}`
// Increment suffix until a unique ID is found
while (nodes.some((node) => node.id === baseId)) {
suffix += 1
baseId = `${nodeData.name}_${suffix}`
}
return `${nodeData.label} ${suffix}`
}
const createAgentFlowOutputs = (nodeData, newNodeId) => {
if (nodeData.hideOutput) return []
if (nodeData.outputs?.length) {
return nodeData.outputs.map((_, index) => ({
id: `${newNodeId}-output-${index}`,
label: nodeData.label,
name: nodeData.name
}))
}
return [
{
id: `${newNodeId}-output-${nodeData.name}`,
label: nodeData.label,
name: nodeData.name
}
]
}
const createOutputOption = (output, newNodeId) => {
const outputBaseClasses = output.baseClasses ?? []
const baseClasses = outputBaseClasses.length > 1 ? outputBaseClasses.join('|') : outputBaseClasses[0] || ''
const type = outputBaseClasses.length > 1 ? outputBaseClasses.join(' | ') : outputBaseClasses[0] || ''
return {
id: `${newNodeId}-output-${output.name}-${baseClasses}`,
name: output.name,
label: output.label,
description: output.description ?? '',
type,
isAnchor: output?.isAnchor,
hidden: output?.hidden
}
}
const createStandardOutputs = (nodeData, newNodeId) => {
if (nodeData.hideOutput) return []
if (nodeData.outputs?.length) {
const outputOptions = nodeData.outputs.map((output) => createOutputOption(output, newNodeId))
return [
{
name: 'output',
label: 'Output',
type: 'options',
description: nodeData.outputs[0].description ?? '',
options: outputOptions,
default: nodeData.outputs[0].name
}
]
}
return [
{
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
name: nodeData.name,
label: nodeData.type,
description: nodeData.description ?? '',
type: nodeData.baseClasses.join(' | ')
}
]
}
const initializeOutputAnchors = (nodeData, newNodeId, isAgentflow) => {
return isAgentflow ? createAgentFlowOutputs(nodeData, newNodeId) : createStandardOutputs(nodeData, newNodeId)
}
export const initializeDefaultNodeData = (nodeParams) => {
const initialValues = {}
@@ -27,17 +115,17 @@ export const initializeDefaultNodeData = (nodeParams) => {
return initialValues
}
export const initNode = (nodeData, newNodeId) => {
export const initNode = (nodeData, newNodeId, isAgentflow) => {
const inputAnchors = []
const inputParams = []
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1
const whitelistTypes = [
'asyncOptions',
'asyncMultiOptions',
'options',
'multiOptions',
'array',
'datagrid',
'string',
'number',
@@ -75,55 +163,7 @@ export const initNode = (nodeData, newNodeId) => {
}
// Outputs
const outputAnchors = []
for (let i = 0; i < outgoing; i += 1) {
if (nodeData.hideOutput) continue
if (nodeData.outputs && nodeData.outputs.length) {
const options = []
for (let j = 0; j < nodeData.outputs.length; j += 1) {
let baseClasses = ''
let type = ''
const outputBaseClasses = nodeData.outputs[j].baseClasses ?? []
if (outputBaseClasses.length > 1) {
baseClasses = outputBaseClasses.join('|')
type = outputBaseClasses.join(' | ')
} else if (outputBaseClasses.length === 1) {
baseClasses = outputBaseClasses[0]
type = outputBaseClasses[0]
}
const newOutputOption = {
id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`,
name: nodeData.outputs[j].name,
label: nodeData.outputs[j].label,
description: nodeData.outputs[j].description ?? '',
type,
isAnchor: nodeData.outputs[j]?.isAnchor,
hidden: nodeData.outputs[j]?.hidden
}
options.push(newOutputOption)
}
const newOutput = {
name: 'output',
label: 'Output',
type: 'options',
description: nodeData.outputs[0].description ?? '',
options,
default: nodeData.outputs[0].name
}
outputAnchors.push(newOutput)
} else {
const newOutput = {
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
name: nodeData.name,
label: nodeData.type,
description: nodeData.description ?? '',
type: nodeData.baseClasses.join(' | ')
}
outputAnchors.push(newOutput)
}
}
let outputAnchors = initializeOutputAnchors(nodeData, newNodeId, isAgentflow)
/* Initial
inputs = [
@@ -160,9 +200,10 @@ export const initNode = (nodeData, newNodeId) => {
// Inputs
if (nodeData.inputs) {
nodeData.inputAnchors = inputAnchors
nodeData.inputParams = inputParams
nodeData.inputs = initializeDefaultNodeData(nodeData.inputs)
const defaultInputs = initializeDefaultNodeData(nodeData.inputs)
nodeData.inputAnchors = showHideInputAnchors({ ...nodeData, inputAnchors, inputs: defaultInputs })
nodeData.inputParams = showHideInputParams({ ...nodeData, inputParams, inputs: defaultInputs })
nodeData.inputs = defaultInputs
} else {
nodeData.inputAnchors = []
nodeData.inputParams = []
@@ -185,8 +226,10 @@ export const initNode = (nodeData, newNodeId) => {
return nodeData
}
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData) => {
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id)
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData, isAgentflow) => {
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id, isAgentflow)
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows' || existingComponentNodeData.category === 'Agent Flows'
// Update credentials with existing credentials
if (existingComponentNodeData.credential) {
@@ -220,6 +263,11 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
}
}
if (isAgentFlowV2) {
// persists the label from the existing node
initNewComponentNodeData.label = existingComponentNodeData.label
}
// Special case for Condition node to update outputAnchors
if (initNewComponentNodeData.name.includes('seqCondition')) {
const options = existingComponentNodeData.outputAnchors[0].options || []
@@ -243,22 +291,34 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
export const updateOutdatedNodeEdge = (newComponentNodeData, edges) => {
const removedEdges = []
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows'
for (const edge of edges) {
const targetNodeId = edge.targetHandle.split('-')[0]
const sourceNodeId = edge.sourceHandle.split('-')[0]
if (targetNodeId === newComponentNodeData.id) {
// Check if targetHandle is in inputParams or inputAnchors
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
if (isAgentFlowV2) {
if (edge.targetHandle !== newComponentNodeData.id) {
removedEdges.push(edge)
}
} else {
// Check if targetHandle is in inputParams or inputAnchors
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
if (!inputParam && !inputAnchor) {
removedEdges.push(edge)
if (!inputParam && !inputAnchor) {
removedEdges.push(edge)
}
}
}
if (sourceNodeId === newComponentNodeData.id) {
if (newComponentNodeData.outputAnchors?.length) {
if (isAgentFlowV2) {
// AgentFlow v2 doesn't have specific output anchors, connections are directly from node
// No need to remove edges for AgentFlow v2 outputs
} else if (newComponentNodeData.outputAnchors?.length) {
for (const outputAnchor of newComponentNodeData.outputAnchors) {
const outputAnchorType = outputAnchor.type
if (outputAnchorType === 'options') {
@@ -315,6 +375,63 @@ export const isValidConnection = (connection, reactFlowInstance) => {
return false
}
export const isValidConnectionAgentflowV2 = (connection, reactFlowInstance) => {
const source = connection.source
const target = connection.target
// Prevent self connections
if (source === target) {
return false
}
// Check if this connection would create a cycle in the graph
if (wouldCreateCycle(source, target, reactFlowInstance)) {
return false
}
return true
}
// Function to check if a new connection would create a cycle
const wouldCreateCycle = (sourceId, targetId, reactFlowInstance) => {
// The most direct cycle check: if target connects back to source
if (sourceId === targetId) {
return true
}
// Build directed graph from existing edges
const graph = {}
const edges = reactFlowInstance.getEdges()
// Initialize graph
edges.forEach((edge) => {
if (!graph[edge.source]) graph[edge.source] = []
graph[edge.source].push(edge.target)
})
// Check if there's a path from target to source (which would create a cycle when we add source → target)
const visited = new Set()
function hasPath(current, destination) {
if (current === destination) return true
if (visited.has(current)) return false
visited.add(current)
const neighbors = graph[current] || []
for (const neighbor of neighbors) {
if (hasPath(neighbor, destination)) {
return true
}
}
return false
}
// If there's a path from target to source, adding an edge from source to target will create a cycle
return hasPath(targetId, sourceId)
}
export const convertDateStringToDateObject = (dateString) => {
if (dateString === undefined || !dateString) return undefined
@@ -367,6 +484,21 @@ export const getFolderName = (base64ArrayStr) => {
}
}
const _removeCredentialId = (obj) => {
if (!obj || typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return obj.map((item) => _removeCredentialId(item))
}
const newObj = {}
for (const [key, value] of Object.entries(obj)) {
if (key === 'FLOWISE_CREDENTIAL_ID') continue
newObj[key] = _removeCredentialId(value)
}
return newObj
}
export const generateExportFlowData = (flowData) => {
const nodes = flowData.nodes
const edges = flowData.edges
@@ -381,6 +513,9 @@ export const generateExportFlowData = (flowData) => {
version: node.data.version,
name: node.data.name,
type: node.data.type,
color: node.data.color,
hideOutput: node.data.hideOutput,
hideInput: node.data.hideInput,
baseClasses: node.data.baseClasses,
tags: node.data.tags,
category: node.data.category,
@@ -406,7 +541,7 @@ export const generateExportFlowData = (flowData) => {
newNodeData.inputs = nodeDataInputs
}
nodes[i].data = newNodeData
nodes[i].data = _removeCredentialId(newNodeData)
}
const exportJson = {
nodes,
@@ -415,11 +550,13 @@ export const generateExportFlowData = (flowData) => {
return exportJson
}
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) => {
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle, includesStart = false) => {
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string|json-llmChain_1-llmChain_1-input-promptValues-string"
// {source} -{sourceHandle} -{target} -{targetHandle}
const parentNodes = []
const isAgentFlowV2 = nodes.find((nd) => nd.id === target)?.data?.category === 'Agent Flows'
const isSeqAgent = nodes.find((nd) => nd.id === target)?.data?.category === 'Sequential Agents'
function collectParentNodes(targetNodeId, nodes, edges) {
@@ -442,10 +579,35 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
}
})
}
function collectAgentFlowV2ParentNodes(targetNodeId, nodes, edges) {
const inputEdges = edges.filter((edg) => edg.target === targetNodeId && edg.targetHandle === targetNodeId)
// Traverse each edge found
inputEdges.forEach((edge) => {
const parentNode = nodes.find((nd) => nd.id === edge.source)
if (!parentNode) return
// Recursive call to explore further up the tree
collectAgentFlowV2ParentNodes(parentNode.id, nodes, edges)
// Check and add the parent node to the list if it does not include specific names
const excludeNodeNames = ['startAgentflow']
if (!excludeNodeNames.includes(parentNode.data.name) || includesStart) {
parentNodes.push(parentNode)
}
})
}
if (isSeqAgent) {
collectParentNodes(target, nodes, edges)
return uniq(parentNodes)
} else if (isAgentFlowV2) {
collectAgentFlowV2ParentNodes(target, nodes, edges)
const parentNodeId = nodes.find((nd) => nd.id === target)?.parentNode
if (parentNodeId) {
collectAgentFlowV2ParentNodes(parentNodeId, nodes, edges)
}
return uniq(parentNodes)
} else {
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
if (inputEdges && inputEdges.length) {
@@ -931,3 +1093,84 @@ export const getCustomConditionOutputs = (value, nodeId, existingEdges, isDataGr
return { outputAnchors, toBeRemovedEdgeIds }
}
const _showHideOperation = (nodeData, inputParam, displayType, index) => {
const displayOptions = inputParam[displayType]
/* For example:
show: {
enableMemory: true
}
*/
Object.keys(displayOptions).forEach((path) => {
const comparisonValue = displayOptions[path]
if (path.includes('$index')) {
path = path.replace('$index', index)
}
const groundValue = get(nodeData.inputs, path, '')
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
}
})
}
export const showHideInputs = (nodeData, inputType, overrideParams, arrayIndex) => {
const params = overrideParams ?? nodeData[inputType] ?? []
for (let i = 0; i < params.length; i += 1) {
const inputParam = params[i]
// Reset display flag to false for each inputParam
inputParam.display = true
if (inputParam.show) {
_showHideOperation(nodeData, inputParam, 'show', arrayIndex)
}
if (inputParam.hide) {
_showHideOperation(nodeData, inputParam, 'hide', arrayIndex)
}
}
return params
}
export const showHideInputParams = (nodeData) => {
return showHideInputs(nodeData, 'inputParams')
}
export const showHideInputAnchors = (nodeData) => {
return showHideInputs(nodeData, 'inputAnchors')
}