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
@@ -0,0 +1,484 @@
import PropTypes from 'prop-types'
import { useContext, memo, useRef, useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { Handle, Position, useUpdateNodeInternals, NodeToolbar } from 'reactflow'
// material-ui
import { styled, useTheme, alpha, darken, lighten } from '@mui/material/styles'
import { ButtonGroup, Avatar, Box, Typography, IconButton, Tooltip } from '@mui/material'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import { flowContext } from '@/store/context/ReactFlowContext'
import NodeInfoDialog from '@/ui-component/dialog/NodeInfoDialog'
// icons
import {
IconCheck,
IconExclamationMark,
IconCircleChevronRightFilled,
IconCopy,
IconTrash,
IconInfoCircle,
IconLoader
} from '@tabler/icons-react'
import StopCircleIcon from '@mui/icons-material/StopCircle'
import CancelIcon from '@mui/icons-material/Cancel'
// const
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
width: 'max-content',
height: 'auto',
padding: '10px',
boxShadow: 'none'
}))
const StyledNodeToolbar = styled(NodeToolbar)(({ theme }) => ({
backgroundColor: theme.palette.card.main,
color: theme.darkTextPrimary,
padding: '5px',
borderRadius: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
}))
// ===========================|| CANVAS NODE ||=========================== //
const AgentFlowNode = ({ data }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
// eslint-disable-next-line
const [position, setPosition] = useState(0)
const [isHovered, setIsHovered] = useState(false)
const { deleteNode, duplicateNode } = useContext(flowContext)
const [showInfoDialog, setShowInfoDialog] = useState(false)
const [infoDialogProps, setInfoDialogProps] = useState({})
const defaultColor = '#666666' // fallback color if data.color is not present
const nodeColor = data.color || defaultColor
// Get different shades of the color based on state
const getStateColor = () => {
if (data.selected) return nodeColor
if (isHovered) return alpha(nodeColor, 0.8)
return alpha(nodeColor, 0.5)
}
const getOutputAnchors = () => {
return data.outputAnchors ?? []
}
const getAnchorPosition = (index) => {
const currentHeight = ref.current?.clientHeight || 0
const spacing = currentHeight / (getOutputAnchors().length + 1)
const position = spacing * (index + 1)
// Update node internals when we get a non-zero position
if (position > 0) {
updateNodeInternals(data.id)
}
return position
}
const getMinimumHeight = () => {
const outputCount = getOutputAnchors().length
// Use exactly 60px as minimum height
return Math.max(60, outputCount * 20 + 40)
}
const getBackgroundColor = () => {
if (customization.isDarkMode) {
return isHovered ? darken(nodeColor, 0.7) : darken(nodeColor, 0.8)
}
return isHovered ? lighten(nodeColor, 0.8) : lighten(nodeColor, 0.9)
}
const getStatusBackgroundColor = (status) => {
switch (status) {
case 'ERROR':
return theme.palette.error.dark
case 'INPROGRESS':
return theme.palette.warning.dark
case 'STOPPED':
case 'TERMINATED':
return theme.palette.error.main
case 'FINISHED':
return theme.palette.success.dark
default:
return theme.palette.primary.dark
}
}
const renderIcon = (node) => {
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.name)
if (!foundIcon) return null
return <foundIcon.icon size={24} color={'white'} />
}
useEffect(() => {
if (ref.current) {
setTimeout(() => {
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
updateNodeInternals(data.id)
}, 10)
}
}, [data, ref, updateNodeInternals])
return (
<div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
<StyledNodeToolbar>
<ButtonGroup sx={{ gap: 1 }} variant='outlined' aria-label='Basic button group'>
{data.name !== 'startAgentflow' && (
<IconButton
size={'small'}
title='Duplicate'
onClick={() => {
duplicateNode(data.id)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.primary.main
}
}}
>
<IconCopy size={20} />
</IconButton>
)}
<IconButton
size={'small'}
title='Delete'
onClick={() => {
deleteNode(data.id)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.error.main
}
}}
>
<IconTrash size={20} />
</IconButton>
<IconButton
size={'small'}
title='Info'
onClick={() => {
setInfoDialogProps({ data })
setShowInfoDialog(true)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.info.main
}
}}
>
<IconInfoCircle size={20} />
</IconButton>
</ButtonGroup>
</StyledNodeToolbar>
<CardWrapper
content={false}
sx={{
borderColor: getStateColor(),
borderWidth: '1px',
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none',
minHeight: getMinimumHeight(),
height: 'auto',
backgroundColor: getBackgroundColor(),
display: 'flex',
alignItems: 'center',
'&:hover': {
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none'
}
}}
border={false}
>
{data && data.status && (
<Tooltip title={data.status === 'ERROR' ? data.error || 'Error' : ''}>
<Avatar
variant='rounded'
sx={{
...theme.typography.smallAvatar,
borderRadius: '50%',
background:
data.status === 'STOPPED' || data.status === 'TERMINATED'
? 'white'
: getStatusBackgroundColor(data.status),
color: 'white',
ml: 2,
position: 'absolute',
top: -10,
right: -10
}}
>
{data.status === 'INPROGRESS' ? (
<IconLoader className='spin-animation' />
) : data.status === 'ERROR' ? (
<IconExclamationMark />
) : data.status === 'TERMINATED' ? (
<CancelIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
) : data.status === 'STOPPED' ? (
<StopCircleIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
) : (
<IconCheck />
)}
</Avatar>
</Tooltip>
)}
<Box sx={{ width: '100%' }}>
{!data.hideInput && (
<Handle
type='target'
position={Position.Left}
id={data.id}
style={{
width: 5,
height: 20,
backgroundColor: 'transparent',
border: 'none',
position: 'absolute',
left: -2
}}
>
<div
style={{
width: 5,
height: 20,
backgroundColor: nodeColor,
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
</Handle>
)}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Box item style={{ width: 50 }}>
{data.color && !data.icon ? (
<div
style={{
...theme.typography.commonAvatar,
...theme.typography.largeAvatar,
borderRadius: '15px',
backgroundColor: data.color,
cursor: 'grab',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: data.color
}}
>
{renderIcon(data)}
</div>
) : (
<div
style={{
...theme.typography.commonAvatar,
...theme.typography.largeAvatar,
borderRadius: '50%',
backgroundColor: 'white',
cursor: 'grab'
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${data.name}`}
alt={data.name}
/>
</div>
)}
</Box>
<Box>
<Typography
sx={{
fontSize: '0.85rem',
fontWeight: 500
}}
>
{data.label}
</Typography>
{(() => {
// Array of model configs to check and render
const modelConfigs = [
{ model: data.inputs?.llmModel, config: data.inputs?.llmModelConfig },
{ model: data.inputs?.agentModel, config: data.inputs?.agentModelConfig },
{ model: data.inputs?.conditionAgentModel, config: data.inputs?.conditionAgentModelConfig }
]
// Filter out undefined models and render each valid one
return modelConfigs
.filter((item) => item.model && item.config)
.map((item, index) => (
<Box key={`model-${index}`} sx={{ display: 'flex', gap: 1, mt: 1 }}>
<Box
sx={{
backgroundColor: customization.isDarkMode
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.9)',
borderRadius: '16px',
width: 'max-content',
height: 24,
pl: 1,
pr: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<img
style={{ width: 20, height: 20, objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${item.model}`}
alt={item.model}
/>
<Typography sx={{ fontSize: '0.7rem', ml: 0.5 }}>
{item.config.modelName || item.config.model}
</Typography>
</Box>
</Box>
))
})()}
{(() => {
// Array of tool configurations to check and render
const toolConfigs = [
{ tools: data.inputs?.llmTools, toolProperty: 'llmSelectedTool' },
{ tools: data.inputs?.agentTools, toolProperty: 'agentSelectedTool' },
{
tools: data.inputs?.selectedTool ? [{ selectedTool: data.inputs?.selectedTool }] : [],
toolProperty: 'selectedTool'
},
{ tools: data.inputs?.agentKnowledgeVSEmbeddings, toolProperty: ['vectorStore', 'embeddingModel'] }
]
// Filter out undefined tools and render each valid collection
return toolConfigs
.filter((config) => config.tools && config.tools.length > 0)
.map((config, configIndex) => (
<Box key={`tools-${configIndex}`} sx={{ display: 'flex', gap: 1, mt: 1 }}>
{config.tools.flatMap((tool, toolIndex) => {
if (Array.isArray(config.toolProperty)) {
return config.toolProperty
.filter((prop) => tool[prop])
.map((prop, propIndex) => {
const toolName = tool[prop]
return (
<Box
key={`tool-${configIndex}-${toolIndex}-${propIndex}`}
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: '50%',
width: 24,
height: 24,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '4px'
}}
>
<img
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${toolName}`}
alt={toolName}
/>
</Box>
)
})
} else {
const toolName = tool[config.toolProperty]
if (!toolName) return []
return [
<Box
key={`tool-${configIndex}-${toolIndex}`}
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: '50%',
width: 24,
height: 24,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '4px'
}}
>
<img
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${toolName}`}
alt={toolName}
/>
</Box>
]
}
})}
</Box>
))
})()}
</Box>
</div>
{getOutputAnchors().map((outputAnchor, index) => {
return (
<Handle
type='source'
position={Position.Right}
key={outputAnchor.id}
id={outputAnchor.id}
style={{
height: 20,
width: 20,
top: getAnchorPosition(index),
backgroundColor: 'transparent',
border: 'none',
position: 'absolute',
right: -10,
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.2s'
}}
>
<div
style={{
position: 'absolute',
width: 20,
height: 20,
borderRadius: '50%',
backgroundColor: theme.palette.background.paper, // or 'white'
pointerEvents: 'none'
}}
/>
<IconCircleChevronRightFilled
size={20}
color={nodeColor}
style={{
pointerEvents: 'none',
position: 'relative',
zIndex: 1
}}
/>
</Handle>
)
})}
</Box>
</CardWrapper>
<NodeInfoDialog show={showInfoDialog} dialogProps={infoDialogProps} onCancel={() => setShowInfoDialog(false)}></NodeInfoDialog>
</div>
)
}
AgentFlowNode.propTypes = {
data: PropTypes.object
}
export default memo(AgentFlowNode)