Feature/seq agents (#2798)

* update build functions

* sequential agents

* update langchain to 0.2, added sequential agent nodes

* add marketplace templates

* update howto wordings

* Merge branch 'main' into feature/Seq-Agents

# Conflicts:
#	pnpm-lock.yaml

* update deprecated functions and add new sequential nodes

* add marketplace templates

* update marketplace templates, add structured output to llm node

* add multi agents template

* update llm node with bindmodels

* update cypress version

* update templates sticky note wordings

* update tool node to include human in loop action

* update structured outputs error from models

* update cohere package to resolve google genai pipeThrough bug

* update mistral package version, added message reconstruction before invoke seq agent

* add HITL to agent

* update state messages restructuring

* update load and split methods for s3 directory
This commit is contained in:
Henry Heng
2024-07-22 17:46:14 +01:00
committed by GitHub
parent 34d0e4302c
commit bca4de0c63
152 changed files with 55307 additions and 35236 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

@@ -0,0 +1,95 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
// MUI
import { Button, Dialog, DialogActions, DialogContent } from '@mui/material'
import { Tabs } from '@mui/base/Tabs'
// Project Import
import { StyledButton } from '@/ui-component/button/StyledButton'
import { TabPanel } from '@/ui-component/tabs/TabPanel'
import { TabsList } from '@/ui-component/tabs/TabsList'
import { Tab } from '@/ui-component/tabs/Tab'
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
// Store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const ConditionDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const [inputParam, setInputParam] = useState(null)
const [tabValue, setTabValue] = useState(0)
const [data, setData] = useState({})
useEffect(() => {
if (dialogProps.inputParam) {
setInputParam(dialogProps.inputParam)
}
if (dialogProps.data) setData(dialogProps.data)
return () => {
setInputParam(null)
setData({})
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
<DialogContent>
<>
{inputParam && inputParam.type.includes('conditionFunction') && (
<>
<Tabs value={tabValue} onChange={(event, val) => setTabValue(val)} aria-label='tabs' variant='fullWidth'>
<TabsList>
{inputParam.tabs.map((inputChildParam, index) => (
<Tab key={index}>{inputChildParam.label}</Tab>
))}
</TabsList>
</Tabs>
{inputParam.tabs.map((inputChildParam, index) => (
<TabPanel key={index} value={tabValue} index={index}>
<NodeInputHandler
disabled={inputChildParam.disabled}
inputParam={inputChildParam}
data={data}
isAdditionalParams={true}
disablePadding={true}
/>
</TabPanel>
))}
</>
)}
</>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(data, inputParam, tabValue)}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ConditionDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default ConditionDialog
@@ -22,7 +22,7 @@ import useApi from '@/hooks/useApi'
import './ExpandTextDialog.css'
const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicked, onConfirm }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
@@ -38,13 +38,18 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode)
useEffect(() => {
if (dialogProps.value) setInputValue(dialogProps.value)
if (dialogProps.value) {
setInputValue(dialogProps.value)
}
if (dialogProps.inputParam) {
setInputParam(dialogProps.inputParam)
if (dialogProps.inputParam.type === 'code') {
setLanguageType('js')
}
}
if (dialogProps.languageType) {
setLanguageType(dialogProps.languageType)
}
return () => {
setInputValue('')
@@ -91,9 +96,22 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
<div style={{ display: 'flex', flexDirection: 'row' }}>
{inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
<div style={{ flex: 70 }}>
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
{inputParam.label}
</Typography>
<div style={{ marginBottom: '10px', display: 'flex', flexDirection: 'row' }}>
<Typography variant='h4'>{inputParam.label}</Typography>
<div style={{ flex: 1 }} />
{inputParam.hint && (
<Button
sx={{ p: 0, px: 2 }}
color='secondary'
variant='text'
onClick={() => {
onInputHintDialogClicked(inputParam.hint)
}}
>
{inputParam.hint.label}
</Button>
)}
</div>
<PerfectScrollbar
style={{
border: '1px solid',
@@ -114,7 +132,12 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
placeholder={inputParam.placeholder}
basicSetup={
languageType !== 'js'
? { lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }
? {
lineNumbers: false,
foldGutter: false,
autocompletion: false,
highlightActiveLine: false
}
: {}
}
onValueChange={(code) => setInputValue(code)}
@@ -123,7 +146,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
</div>
)}
</div>
{languageType === 'js' && (
{languageType === 'js' && !inputParam.hideCodeExecute && (
<LoadingButton
sx={{
mt: 2,
@@ -177,7 +200,8 @@ ExpandTextDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
onConfirm: PropTypes.func,
onInputHintDialogClicked: PropTypes.func
}
export default ExpandTextDialog
@@ -46,6 +46,7 @@ const FormatPromptValuesDialog = ({ show, dialogProps, onChange, onCancel }) =>
nodes={dialogProps.nodes}
edges={dialogProps.edges}
nodeId={dialogProps.nodeId}
isSequentialAgent={dialogProps.data.category === 'Sequential Agents'}
/>
</PerfectScrollbar>
</DialogContent>
@@ -0,0 +1,63 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
const InputHintDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.label}
</DialogTitle>
<DialogContent>
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
isDialog={true}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{dialogProps?.value}
</MemoizedReactMarkdown>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
InputHintDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default InputHintDialog
@@ -34,7 +34,7 @@ import userPNG from '@/assets/images/account.png'
import msgEmptySVG from '@/assets/images/message_empty.svg'
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons-react'
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons-react'
// Project import
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
@@ -846,6 +846,64 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</Box>
<div>{agent.agentName}</div>
</Stack>
{agent.usedTools && agent.usedTools.length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{agent.usedTools.map((tool, index) => {
return tool !== null ? (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={<IconTool size={15} />}
onClick={() =>
onSourceDialogClick(
tool,
'Used Tools'
)
}
/>
) : null
})}
</div>
)}
{agent.state &&
Object.keys(agent.state).length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
<Chip
size='small'
label={'State'}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={
<IconDeviceSdCard size={15} />
}
onClick={() =>
onSourceDialogClick(
agent.state,
'State'
)
}
/>
</div>
)}
{agent.messages.length > 0 && (
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
@@ -893,6 +951,67 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
{agent.instructions && <p>{agent.instructions}</p>}
{agent.messages.length === 0 &&
!agent.instructions && <p>Finished</p>}
{agent.sourceDocuments &&
agent.sourceDocuments.length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{removeDuplicateURL(agent).map(
(source, index) => {
const URL =
source &&
source.metadata &&
source.metadata.source
? isValidURL(
source.metadata
.source
)
: undefined
return (
<Chip
size='small'
key={index}
label={
URL
? URL.pathname.substring(
0,
15
) === '/'
? URL.host
: `${URL.pathname.substring(
0,
15
)}...`
: `${source.pageContent.substring(
0,
15
)}...`
}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() =>
URL
? onURLClick(
source
.metadata
.source
)
: onSourceDialogClick(
source
)
}
/>
)
}
)}
</div>
)}
</CardContent>
</Card>
)
+27 -1
View File
@@ -6,6 +6,32 @@ import { Button } from '@mui/material'
import DeleteIcon from '@mui/icons-material/Delete'
import { cloneDeep } from 'lodash'
import { formatDataGridRows } from '@/utils/genericHelper'
import { styled } from '@mui/material/styles'
const StyledDataGrid = styled(MUIDataGrid)(({ theme }) => ({
border: `1px solid ${theme.palette.mode === 'light' ? '#b4b4b4' : '#303030'}`,
letterSpacing: 'normal',
'& .MuiDataGrid-columnsContainer': {
backgroundColor: theme.palette.mode === 'light' ? '#fafafa' : '#1d1d1d'
},
'& .MuiDataGrid-iconSeparator': {
display: 'none'
},
'& .MuiDataGrid-columnHeader, .MuiDataGrid-cell': {
borderRight: `1px solid ${theme.palette.mode === 'light' ? '#f0f0f0' : '#303030'}`
},
'& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell': {
borderBottom: `1px solid ${theme.palette.mode === 'light' ? '#f0f0f0' : '#303030'}`
},
'& .MuiPaginationItem-root': {
borderRadius: 0
},
'& .MuiDataGrid-columnHeader:last-child, .MuiDataGrid-cell:last-child': {
borderRight: 'none'
}
}))
export const DataGrid = ({ columns, rows, style, disabled = false, hideFooter = false, onChange }) => {
const [rowValues, setRowValues] = useState(formatDataGridRows(rows) ?? [])
@@ -80,7 +106,7 @@ export const DataGrid = ({ columns, rows, style, disabled = false, hideFooter =
<>
{rowValues && colValues && (
<div style={{ marginTop: 10, height: 210, width: '100%', ...style }}>
<MUIDataGrid
<StyledDataGrid
processRowUpdate={handleProcessRowUpdate}
isCellEditable={() => {
return !disabled
@@ -6,7 +6,17 @@ import SelectVariable from './SelectVariable'
import { cloneDeep } from 'lodash'
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
export const JsonEditorInput = ({ value, onChange, inputParam, nodes, edges, nodeId, disabled = false, isDarkMode = false }) => {
export const JsonEditorInput = ({
value,
onChange,
inputParam,
nodes,
edges,
nodeId,
disabled = false,
isDarkMode = false,
isSequentialAgent = false
}) => {
const [myValue, setMyValue] = useState(value ? JSON.parse(value) : {})
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
const [mouseUpKey, setMouseUpKey] = useState('')
@@ -110,6 +120,7 @@ export const JsonEditorInput = ({ value, onChange, inputParam, nodes, edges, nod
setNewVal(val)
handleClosePopOver()
}}
isSequentialAgent={isSequentialAgent}
/>
</Popover>
)}
@@ -125,5 +136,6 @@ JsonEditorInput.propTypes = {
inputParam: PropTypes.object,
nodes: PropTypes.array,
edges: PropTypes.array,
nodeId: PropTypes.string
nodeId: PropTypes.string,
isSequentialAgent: PropTypes.bool
}
@@ -4,9 +4,29 @@ import { Box, List, ListItemButton, ListItem, ListItemAvatar, ListItemText, Typo
import PerfectScrollbar from 'react-perfect-scrollbar'
import robotPNG from '@/assets/images/robot.png'
import chatPNG from '@/assets/images/chathistory.png'
import diskPNG from '@/assets/images/floppy-disc.png'
import { baseURL } from '@/store/constant'
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal }) => {
const sequentialStateMessagesSelection = [
{
primary: '$flow.state.messages',
secondary: `All messages from the start of the conversation till now`
},
{
primary: '$flow.state.<replace-with-key>',
secondary: `Current value of the state variable with specified key`
},
{
primary: '$flow.state.messages[0].content',
secondary: `First message content`
},
{
primary: '$flow.state.messages[-1].content',
secondary: `Last message content`
}
]
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal, isSequentialAgent }) => {
const customization = useSelector((state) => state.customization)
const onSelectOutputResponseClick = (node, prefix) => {
@@ -102,9 +122,10 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
{availableNodesForVariable &&
availableNodesForVariable.length > 0 &&
availableNodesForVariable.map((node, index) => {
const selectedOutputAnchor = node.data.outputAnchors[0].options.find(
(ancr) => ancr.name === node.data.outputs['output']
)
const selectedOutputAnchor =
node.data.outputAnchors.length &&
node.data.outputAnchors[0].options &&
node.data.outputAnchors[0].options.find((ancr) => ancr.name === node.data.outputs['output'])
return (
<ListItemButton
key={index}
@@ -157,6 +178,45 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
</ListItemButton>
)
})}
{isSequentialAgent &&
(sequentialStateMessagesSelection || []).map((item, index) => (
<ListItemButton
key={index}
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
mb: 1
}}
disabled={disabled}
onClick={() => onSelectAndReturnVal(item.primary)}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
}}
alt='state'
src={diskPNG}
/>
</div>
</ListItemAvatar>
<ListItemText sx={{ ml: 1 }} primary={item.primary} secondary={item.secondary} />
</ListItem>
</ListItemButton>
))}
</List>
</Box>
</PerfectScrollbar>
@@ -169,7 +229,8 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
SelectVariable.propTypes = {
availableNodesForVariable: PropTypes.array,
disabled: PropTypes.bool,
onSelectAndReturnVal: PropTypes.func
onSelectAndReturnVal: PropTypes.func,
isSequentialAgent: PropTypes.bool
}
export default SelectVariable
@@ -0,0 +1,21 @@
.react-markdown table {
border-spacing: 0 !important;
border-collapse: collapse !important;
border-color: inherit !important;
display: block !important;
width: max-content !important;
max-width: 100% !important;
overflow: auto !important;
}
.react-markdown tbody,
.react-markdown td,
.react-markdown tfoot,
.react-markdown th,
.react-markdown thead,
.react-markdown tr {
border-color: inherit !important;
border-style: solid !important;
border-width: 1px !important;
padding: 10px !important;
}
@@ -1,4 +1,19 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import ReactMarkdown from 'react-markdown'
import './Markdown.css'
export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children)
export const MemoizedReactMarkdown = memo(
({ children, ...props }) => (
<div className='react-markdown'>
<ReactMarkdown {...props}>{children}</ReactMarkdown>
</div>
),
(prevProps, nextProps) => prevProps.children === nextProps.children
)
MemoizedReactMarkdown.displayName = 'MemoizedReactMarkdown'
MemoizedReactMarkdown.propTypes = {
children: PropTypes.any
}
+42
View File
@@ -0,0 +1,42 @@
import { styled } from '@mui/system'
import { buttonClasses } from '@mui/base/Button'
import { Tab as BaseTab, tabClasses } from '@mui/base/Tab'
import { blue } from './tabColors'
export const Tab = styled(BaseTab)(
({ ...props }) => `
font-family: 'IBM Plex Sans', sans-serif;
color: white;
cursor: pointer;
font-size: 0.75rem;
font-weight: bold;
background-color: transparent;
width: 100%;
line-height: 1.5;
padding: 8px 12px;
margin: 6px;
border: none;
border-radius: 25px;
display: flex;
justify-content: center;
&:hover {
background-color: ${props.sx?.backgroundColor || blue[400]};
}
&:focus {
color: #fff;
outline: 3px solid ${props.sx?.backgroundColor || blue[200]};
}
&.${tabClasses.selected} {
background-color: #fff;
color: ${blue[600]};
}
&.${buttonClasses.disabled} {
opacity: 0.5;
cursor: not-allowed;
}
`
)
@@ -0,0 +1,17 @@
import PropTypes from 'prop-types'
import { Box } from '@mui/material'
export const TabPanel = (props) => {
const { children, value, index, ...other } = props
return (
<div role='tabpanel' hidden={value !== index} id={`tabpanel-${index}`} aria-labelledby={`tab-${index}`} {...other}>
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
</div>
)
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
}
@@ -0,0 +1,18 @@
import { styled } from '@mui/system'
import { TabsList as BaseTabsList } from '@mui/base/TabsList'
import { blue } from './tabColors'
export const TabsList = styled(BaseTabsList)(
({ theme, ...props }) => `
min-width: 400px;
background-color: ${props.sx?.backgroundColor || blue[500]};
border-radius: 20px;
margin-top: 16px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
align-content: space-between;
box-shadow: 0px 4px 6px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.4)' : 'rgba(0,0,0, 0.2)'};
`
)
@@ -0,0 +1,12 @@
export const blue = {
50: '#F0F7FF',
100: '#C2E0FF',
200: '#80BFFF',
300: '#66B2FF',
400: '#3399FF',
500: '#007FFF',
600: '#0072E5',
700: '#0059B2',
800: '#004C99',
900: '#003A75'
}
+156 -9
View File
@@ -1,4 +1,5 @@
import moment from 'moment'
import { uniq } from 'lodash'
export const getUniqueNodeId = (nodeData, nodes) => {
// Get amount of same nodes
@@ -52,7 +53,9 @@ export const initNode = (nodeData, newNodeId) => {
'code',
'date',
'file',
'folder'
'folder',
'tabs',
'conditionFunction' // This is a special type for condition functions
]
// Inputs
@@ -80,6 +83,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) {
@@ -100,7 +104,9 @@ export const initNode = (nodeData, newNodeId) => {
name: nodeData.outputs[j].name,
label: nodeData.outputs[j].label,
description: nodeData.outputs[j].description ?? '',
type
type,
isAnchor: nodeData.outputs[j]?.isAnchor,
hidden: nodeData.outputs[j]?.hidden
}
options.push(newOutputOption)
}
@@ -404,14 +410,43 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
// 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 inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
if (inputEdges && inputEdges.length) {
for (let j = 0; j < inputEdges.length; j += 1) {
const node = nodes.find((nd) => nd.id === inputEdges[j].source)
parentNodes.push(node)
}
const isSeqAgent = nodes.find((nd) => nd.id === target)?.data?.category === 'Sequential Agents'
function collectParentNodes(targetNodeId, nodes, edges) {
const inputEdges = edges.filter(
(edg) => edg.target === targetNodeId && edg.targetHandle.includes(`${targetNodeId}-input-sequentialNode`)
)
// 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
collectParentNodes(parentNode.id, nodes, edges)
// Check and add the parent node to the list if it does not include specific names
const excludeNodeNames = ['seqAgent', 'seqLLMNode', 'seqToolNode']
if (excludeNodeNames.includes(parentNode.data.name)) {
parentNodes.push(parentNode)
}
})
}
if (isSeqAgent) {
collectParentNodes(target, nodes, edges)
return uniq(parentNodes)
} else {
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
if (inputEdges && inputEdges.length) {
for (let j = 0; j < inputEdges.length; j += 1) {
const node = nodes.find((nd) => nd.id === inputEdges[j].source)
parentNodes.push(node)
}
}
return parentNodes
}
return parentNodes
}
export const getUpsertDetails = (nodes, edges) => {
@@ -783,3 +818,115 @@ export const kFormatter = (num) => {
const item = lookup.findLast((item) => num >= item.value)
return item ? (num / item.value).toFixed(1).replace(regexp, '').concat(item.symbol) : '0'
}
const toCamelCase = (str) => {
return str
.split(' ') // Split by space to process each word
.map((word, index) => (index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()))
.join('') // Join the words back into a single string
}
const createJsonArray = (labels) => {
return labels.map((label) => {
return {
label: label,
name: toCamelCase(label),
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
isAnchor: true
}
})
}
export const getCustomConditionOutputs = (value, nodeId, existingEdges, isDataGrid) => {
// Regex to find return statements and capture returned values
const regex = /return\s+(['"`])(.*?)\1/g
let match
const numberOfReturns = []
if (!isDataGrid) {
// Loop over the matches of the regex
while ((match = regex.exec(value)) !== null) {
// Push the captured group, which is the actual return value, into results
numberOfReturns.push(match[2])
}
} else {
try {
const parsedValue = JSON.parse(value)
if (parsedValue && parsedValue.length) {
for (const item of parsedValue) {
if (!item.variable) {
alert('Please specify a Variable. Try connecting Condition node to a previous node and select the variable')
return undefined
}
if (!item.output) {
alert('Please specify an Output Name')
return undefined
}
if (!item.operation) {
alert('Please select an operation for the condition')
return undefined
}
numberOfReturns.push(item.output)
}
numberOfReturns.push('End')
}
} catch (e) {
console.error('Error parsing JSON', e)
}
}
if (numberOfReturns.length === 0) {
if (isDataGrid) alert('Please add an item for the condition')
else
alert(
'Please add a return statement in the condition code to define the output. You can refer to How to Use for more information.'
)
return undefined
}
const outputs = createJsonArray(numberOfReturns.sort())
const outputAnchors = []
const options = []
for (let j = 0; j < outputs.length; j += 1) {
let baseClasses = ''
let type = ''
const outputBaseClasses = 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: `${nodeId}-output-${outputs[j].name}-${baseClasses}`,
name: outputs[j].name,
label: outputs[j].label,
type,
isAnchor: outputs[j]?.isAnchor
}
options.push(newOutputOption)
}
const newOutput = {
name: 'output',
label: 'Output',
type: 'options',
options
}
outputAnchors.push(newOutput)
// Remove edges
let newEdgeSourceHandles = []
for (const anchor of options) {
const anchorId = anchor.id
newEdgeSourceHandles.push(anchorId)
}
const toBeRemovedEdgeIds = existingEdges.filter((edge) => !newEdgeSourceHandles.includes(edge.sourceHandle)).map((edge) => edge.id)
return { outputAnchors, toBeRemovedEdgeIds }
}
+21 -2
View File
@@ -56,6 +56,9 @@ function a11yProps(index) {
const blacklistCategoriesForAgentCanvas = ['Agents', 'Memory', 'Record Manager']
const allowedAgentModel = {}
const exceptions = {
Memory: ['agentMemory']
}
const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
const theme = useTheme()
@@ -84,9 +87,19 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
filterSearch(searchValue, newValue)
}
const addException = () => {
let nodes = []
for (const category in exceptions) {
const nodeNames = exceptions[category]
nodes.push(...nodesData.filter((nd) => nd.category === category && nodeNames.includes(nd.name)))
}
return nodes
}
const getSearchedNodes = (value) => {
if (isAgentCanvas) {
const nodes = nodesData.filter((nd) => !blacklistCategoriesForAgentCanvas.includes(nd.category))
nodes.push(...addException())
const passed = nodes.filter((nd) => {
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
@@ -94,7 +107,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
})
return passed
}
const nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents')
const nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents' && nd.category !== 'Sequential Agents')
const passed = nodes.filter((nd) => {
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
@@ -156,9 +169,15 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
filteredResult[category] = nodes
}
}
// Allow exceptions
if (Object.keys(exceptions).includes(category)) {
filteredResult[category] = addException()
}
}
setNodes(filteredResult)
accordianCategories['Multi Agents'] = true
accordianCategories['Sequential Agents'] = true
setCategoryExpanded(accordianCategories)
} else {
const taggedNodes = groupByTags(nodes, newTabValue)
@@ -172,7 +191,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
const filteredResult = {}
for (const category in result) {
if (category === 'Multi Agents') {
if (category === 'Multi Agents' || category === 'Sequential Agents') {
continue
}
filteredResult[category] = result[category]
+37 -17
View File
@@ -34,6 +34,7 @@ const CanvasNode = ({ data }) => {
const [infoDialogProps, setInfoDialogProps] = useState({})
const [warningMessage, setWarningMessage] = useState('')
const [open, setOpen] = useState(false)
const [isForceCloseNodeInfo, setIsForceCloseNodeInfo] = useState(null)
const handleClose = () => {
setOpen(false)
@@ -43,6 +44,11 @@ const CanvasNode = ({ data }) => {
setOpen(true)
}
const getNodeInfoOpenStatus = () => {
if (isForceCloseNodeInfo) return false
else return !canvas.canvasDialogShow && open
}
const nodeOutdatedMessage = (oldVersion, newVersion) => `Node version ${oldVersion} outdated\nUpdate to latest version ${newVersion}`
const nodeVersionEmptyMessage = (newVersion) => `Node outdated\nUpdate to latest version ${newVersion}`
@@ -87,7 +93,7 @@ const CanvasNode = ({ data }) => {
border={false}
>
<NodeTooltip
open={!canvas.canvasDialogShow && open}
open={getNodeInfoOpenStatus()}
onClose={handleClose}
onOpen={handleOpen}
disableFocusListener={true}
@@ -213,7 +219,18 @@ const CanvasNode = ({ data }) => {
{data.inputParams
.filter((inputParam) => !inputParam.hidden)
.map((inputParam, index) => (
<NodeInputHandler key={index} inputParam={inputParam} data={data} />
<NodeInputHandler
key={index}
inputParam={inputParam}
data={data}
onHideNodeInfoDialog={(status) => {
if (status) {
setIsForceCloseNodeInfo(true)
} else {
setIsForceCloseNodeInfo(null)
}
}}
/>
))}
{data.inputParams.find((param) => param.additionalParams) && (
<div
@@ -231,21 +248,24 @@ const CanvasNode = ({ data }) => {
</Button>
</div>
)}
<Divider />
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Output
</Typography>
</Box>
<Divider />
{data.outputAnchors.map((outputAnchor) => (
<NodeOutputHandler key={JSON.stringify(data)} outputAnchor={outputAnchor} data={data} />
))}
{data.outputAnchors.length > 0 && <Divider />}
{data.outputAnchors.length > 0 && (
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Output
</Typography>
</Box>
)}
{data.outputAnchors.length > 0 && <Divider />}
{data.outputAnchors.length > 0 &&
data.outputAnchors.map((outputAnchor) => (
<NodeOutputHandler key={JSON.stringify(data)} outputAnchor={outputAnchor} data={data} />
))}
</Box>
</NodeTooltip>
</NodeCardWrapper>
+340 -18
View File
@@ -5,10 +5,13 @@ import { useSelector } from 'react-redux'
// material-ui
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
import { Popper, Box, Typography, Tooltip, IconButton, Button, TextField } from '@mui/material'
import { useGridApiContext } from '@mui/x-data-grid'
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons-react'
import { IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb } from '@tabler/icons-react'
import { Tabs } from '@mui/base/Tabs'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
// project import
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
@@ -19,20 +22,24 @@ import { DataGrid } from '@/ui-component/grid/DataGrid'
import { File } from '@/ui-component/file/File'
import { SwitchInput } from '@/ui-component/switch/Switch'
import { flowContext } from '@/store/context/ReactFlowContext'
import { isValidConnection } from '@/utils/genericHelper'
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import { TabPanel } from '@/ui-component/tabs/TabPanel'
import { TabsList } from '@/ui-component/tabs/TabsList'
import { Tab } from '@/ui-component/tabs/Tab'
import ToolDialog from '@/views/tools/ToolDialog'
import AssistantDialog from '@/views/assistants/AssistantDialog'
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
import CredentialInputHandler from './CredentialInputHandler'
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
// utils
import { getInputVariables } from '@/utils/genericHelper'
import { getInputVariables, getCustomConditionOutputs, isValidConnection, getAvailableNodesForVariable } from '@/utils/genericHelper'
// const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
@@ -45,13 +52,33 @@ const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...prop
}
})
const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
borderRadius: '10px',
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 10,
margin: 10
}
}
})
// ===========================|| NodeInputHandler ||=========================== //
const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isAdditionalParams = false }) => {
const NodeInputHandler = ({
inputAnchor,
inputParam,
data,
disabled = false,
isAdditionalParams = false,
disablePadding = false,
onHideNodeInfoDialog
}) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const ref = useRef(null)
const { reactFlowInstance } = useContext(flowContext)
const { reactFlowInstance, deleteEdge } = useContext(flowContext)
const updateNodeInternals = useUpdateNodeInternals()
const [position, setPosition] = useState(0)
const [showExpandDialog, setShowExpandDialog] = useState(false)
@@ -64,12 +91,26 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [showPromptHubDialog, setShowPromptHubDialog] = useState(false)
const [showManageScrapedLinksDialog, setShowManageScrapedLinksDialog] = useState(false)
const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({})
const [showInputHintDialog, setShowInputHintDialog] = useState(false)
const [inputHintDialogProps, setInputHintDialogProps] = useState({})
const [showConditionDialog, setShowConditionDialog] = useState(false)
const [conditionDialogProps, setConditionDialogProps] = useState({})
const [tabValue, setTabValue] = useState(0)
const onExpandDialogClicked = (value, inputParam) => {
const onInputHintDialogClicked = (hint) => {
const dialogProps = {
...hint
}
setInputHintDialogProps(dialogProps)
setShowInputHintDialog(true)
}
const onExpandDialogClicked = (value, inputParam, languageType) => {
const dialogProps = {
value,
inputParam,
disabled,
languageType,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
@@ -77,6 +118,19 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
setShowExpandDialog(true)
}
const onConditionDialogClicked = (inputParam) => {
const dialogProps = {
data,
inputParam,
disabled,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setConditionDialogProps(dialogProps)
setShowConditionDialog(true)
onHideNodeInfoDialog(true)
}
const onShowPromptHubButtonClicked = () => {
setShowPromptHubDialog(true)
}
@@ -120,6 +174,157 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
return ''
}
const getDataGridColDef = (columns, inputParam) => {
const colDef = []
for (const column of columns) {
const stateNode = reactFlowInstance ? reactFlowInstance.getNodes().find((node) => node.data.name === 'seqState') : null
if (column.type === 'asyncSingleSelect' && column.loadMethod && column.loadMethod.includes('loadStateKeys')) {
if (stateNode) {
const tabParam = stateNode.data.inputParams.find((param) => param.tabIdentifier)
if (tabParam && tabParam.tabs.length > 0) {
const selectedTabIdentifier = tabParam.tabIdentifier
const selectedTab =
stateNode.data.inputs[`${selectedTabIdentifier}_${stateNode.data.id}`] ||
tabParam.default ||
tabParam.tabs[0].name
const datagridValues = stateNode.data.inputs[selectedTab]
if (datagridValues) {
try {
const parsedDatagridValues = JSON.parse(datagridValues)
const keys = Array.isArray(parsedDatagridValues)
? parsedDatagridValues.map((item) => item.key)
: Object.keys(parsedDatagridValues)
colDef.push({
...column,
field: column.field,
headerName: column.headerName,
type: 'singleSelect',
editable: true,
valueOptions: keys
})
} catch (error) {
console.error('Error parsing stateMemory', error)
}
}
}
} else {
colDef.push({
...column,
field: column.field,
headerName: column.headerName,
type: 'singleSelect',
editable: true,
valueOptions: []
})
}
} else if (column.type === 'freeSolo') {
const preLoadOptions = []
if (column.loadMethod && column.loadMethod.includes('getPreviousMessages')) {
const nodes = getAvailableNodesForVariable(
reactFlowInstance?.getNodes() || [],
reactFlowInstance?.getEdges() || [],
data.id,
inputParam.id
)
for (const node of nodes) {
preLoadOptions.push({
value: `$${node.id}`,
label: `Output from ${node.data.id}`
})
}
}
if (column.loadMethod && column.loadMethod.includes('loadStateKeys')) {
if (stateNode) {
const tabParam = stateNode.data.inputParams.find((param) => param.tabIdentifier)
if (tabParam && tabParam.tabs.length > 0) {
const selectedTabIdentifier = tabParam.tabIdentifier
const selectedTab =
stateNode.data.inputs[`${selectedTabIdentifier}_${stateNode.data.id}`] ||
tabParam.default ||
tabParam.tabs[0].name
const datagridValues = stateNode.data.inputs[selectedTab]
if (datagridValues) {
try {
const parsedDatagridValues = JSON.parse(datagridValues)
const keys = Array.isArray(parsedDatagridValues)
? parsedDatagridValues.map((item) => item.key)
: Object.keys(parsedDatagridValues)
for (const key of keys) {
preLoadOptions.push({
value: `$flow.state.${key}`,
label: `Value from ${key}`
})
}
} catch (error) {
console.error('Error parsing stateMemory', error)
}
}
}
}
}
colDef.push({
...column,
field: column.field,
headerName: column.headerName,
renderEditCell: ({ id, field, value }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const apiRef = useGridApiContext()
return (
<Autocomplete
id={column.field}
freeSolo
fullWidth
options={[...preLoadOptions, ...column.valueOptions]}
value={value}
PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props}>
<div>
<strong>{option.value}</strong>
<br />
<small>{option.label}</small>
</div>
</li>
)}
getOptionLabel={(option) => {
return typeof option === 'string' ? option : option.value
}}
onInputChange={(event, newValue) => {
apiRef.current.setEditCellValue({ id, field, value: newValue })
}}
sx={{
'& .MuiInputBase-root': {
height: '50px' // Adjust this value as needed
},
'& .MuiOutlinedInput-root': {
border: 'none'
},
'& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
}}
/>
)
}
})
} else {
colDef.push(column)
}
}
return colDef
}
const getTabValue = (inputParam) => {
return inputParam.tabs.findIndex((item) => item.name === data.inputs[`${inputParam.tabIdentifier}_${data.id}`]) >= 0
? inputParam.tabs.findIndex((item) => item.name === data.inputs[`${inputParam.tabIdentifier}_${data.id}`])
: tabValue
}
const onEditJSONClicked = (value, inputParam) => {
// Preset values if the field is format prompt values
let inputValue = value
@@ -134,17 +339,37 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const dialogProp = {
value: inputValue,
inputParam,
nodes: reactFlowInstance.getNodes(),
edges: reactFlowInstance.getEdges(),
nodeId: data.id
nodes: reactFlowInstance?.getNodes() || [],
edges: reactFlowInstance?.getEdges() || [],
nodeId: data.id,
data
}
setFormatPromptValuesDialogProps(dialogProp)
setShowFormatPromptValuesDialog(true)
}
const onExpandDialogSave = (newValue, inputParamName) => {
setShowExpandDialog(false)
data.inputs[inputParamName] = newValue
setShowExpandDialog(false)
}
const onConditionDialogSave = (newData, inputParam, tabValue) => {
data.inputs[`${inputParam.tabIdentifier}_${data.id}`] = inputParam.tabs[tabValue].name
const existingEdges = reactFlowInstance?.getEdges().filter((edge) => edge.source === data.id) || []
const { outputAnchors, toBeRemovedEdgeIds } = getCustomConditionOutputs(
newData.inputs[inputParam.tabs[tabValue].name],
data.id,
existingEdges,
inputParam.tabs[tabValue].type === 'datagrid'
)
if (!outputAnchors) return
data.outputAnchors = outputAnchors
for (const edgeId of toBeRemovedEdgeIds) {
deleteEdge(edgeId)
}
setShowConditionDialog(false)
onHideNodeInfoDialog(false)
}
const editAsyncOption = (inputParamName, inputValue) => {
@@ -240,7 +465,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{((inputParam && !inputParam.additionalParams) || isAdditionalParams) && (
<>
{inputParam.acceptVariable && (
{inputParam.acceptVariable && !isAdditionalParams && (
<CustomWidthTooltip placement='left' title={inputParam.type}>
<Handle
type='target'
@@ -257,7 +482,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
/>
</CustomWidthTooltip>
)}
<Box sx={{ p: 2 }}>
<Box sx={{ p: disablePadding ? 0 : 2 }}>
{(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') &&
(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && (
<>
@@ -290,6 +515,19 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{inputParam.hint && isAdditionalParams && (
<Button
sx={{ p: 0, px: 2 }}
color='secondary'
variant='text'
onClick={() => {
onInputHintDialogClicked(inputParam.hint)
}}
startIcon={<IconBulb size={17} />}
>
{inputParam.hint.label}
</Button>
)}
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
<IconButton
size='small'
@@ -335,7 +573,37 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
}}
/>
)}
{inputParam.type === 'tabs' && (
<>
<Tabs
value={getTabValue(inputParam)}
onChange={(event, val) => {
setTabValue(val)
data.inputs[`${inputParam.tabIdentifier}_${data.id}`] = inputParam.tabs[val].name
}}
aria-label='tabs'
variant='fullWidth'
defaultValue={getTabValue(inputParam)}
>
<TabsList>
{inputParam.tabs.map((inputChildParam, index) => (
<Tab key={index}>{inputChildParam.label}</Tab>
))}
</TabsList>
</Tabs>
{inputParam.tabs.map((inputChildParam, index) => (
<TabPanel key={index} value={getTabValue(inputParam)} index={index}>
<NodeInputHandler
disabled={inputChildParam.disabled}
inputParam={inputChildParam}
data={data}
isAdditionalParams={true}
disablePadding={true}
/>
</TabPanel>
))}
</>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
@@ -354,7 +622,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam.type === 'datagrid' && (
<DataGrid
disabled={disabled}
columns={inputParam.datagrid}
columns={getDataGridColDef(inputParam.datagrid, inputParam)}
hideFooter={true}
rows={data.inputs[inputParam.name] ?? JSON.stringify(inputParam.default) ?? []}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
@@ -362,8 +630,27 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
)}
{inputParam.type === 'code' && (
<>
<div style={{ height: '5px' }}></div>
<div style={{ height: inputParam.rows ? '100px' : '200px' }}>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
{inputParam.codeExample && (
<Button
variant='outlined'
onClick={() => {
data.inputs[inputParam.name] = inputParam.codeExample
}}
>
See Example
</Button>
)}
</div>
<div
style={{
marginTop: '10px',
border: '1px solid',
borderColor: theme.palette.grey['300'],
borderRadius: '6px',
height: inputParam.rows ? '100px' : '200px'
}}
>
<CodeEditor
disabled={disabled}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
@@ -401,6 +688,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
getJSONValue(data.inputs['workerPrompt']) ||
''
}
isSequentialAgent={data.category === 'Sequential Agents'}
isDarkMode={customization.isDarkMode}
/>
)}
@@ -473,6 +761,23 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
</div>
</>
)}
{/* CUSTOM INPUT LOGIC */}
{inputParam.type.includes('conditionFunction') && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
sx={{ borderRadius: '12px', width: '100%', mt: 1 }}
variant='outlined'
onClick={() => onConditionDialogClicked(inputParam)}
>
{inputParam.label}
</Button>
</>
)}
{(data.name === 'cheerioWebScraper' ||
data.name === 'puppeteerWebScraper' ||
data.name === 'playwrightWebScraper') &&
@@ -526,7 +831,22 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
dialogProps={expandDialogProps}
onCancel={() => setShowExpandDialog(false)}
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
onInputHintDialogClicked={onInputHintDialogClicked}
></ExpandTextDialog>
<ConditionDialog
show={showConditionDialog}
dialogProps={conditionDialogProps}
onCancel={() => {
setShowConditionDialog(false)
onHideNodeInfoDialog(false)
}}
onConfirm={(newData, inputParam, tabValue) => onConditionDialogSave(newData, inputParam, tabValue)}
></ConditionDialog>
<InputHintDialog
show={showInputHintDialog}
dialogProps={inputHintDialogProps}
onCancel={() => setShowInputHintDialog(false)}
></InputHintDialog>
</div>
)
}
@@ -536,7 +856,9 @@ NodeInputHandler.propTypes = {
inputParam: PropTypes.object,
data: PropTypes.object,
disabled: PropTypes.bool,
isAdditionalParams: PropTypes.bool
isAdditionalParams: PropTypes.bool,
disablePadding: PropTypes.bool,
onHideNodeInfoDialog: PropTypes.func
}
export default NodeInputHandler
@@ -23,12 +23,29 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
const [position, setPosition] = useState(0)
const [clientHeight, setClientHeight] = useState(0)
const [offsetTop, setOffsetTop] = useState(0)
const [dropdownValue, setDropdownValue] = useState(null)
const { reactFlowInstance } = useContext(flowContext)
const getAvailableOptions = (options = []) => {
return options.filter((option) => !option.hidden && !option.isAnchor)
}
const getAnchorOptions = (options = []) => {
return options.filter((option) => !option.hidden && option.isAnchor)
}
const getAnchorPosition = (options, index) => {
const spacing = clientHeight / (getAnchorOptions(options).length + 1)
return offsetTop + spacing * (index + 1)
}
useEffect(() => {
if (ref.current && ref.current?.offsetTop && ref.current?.clientHeight) {
setTimeout(() => {
setClientHeight(ref.current?.clientHeight)
setOffsetTop(ref.current?.offsetTop)
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
updateNodeInternals(data.id)
}, 0)
@@ -73,6 +90,38 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
</Box>
</>
)}
{data.name !== 'ifElseFunction' &&
outputAnchor.type === 'options' &&
outputAnchor.options &&
getAnchorOptions(outputAnchor.options).length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{getAnchorOptions(outputAnchor.options).map((option, index) => {
return (
<div key={option.id} style={{ display: 'flex', flexDirection: 'row' }}>
<CustomWidthTooltip placement='right' title={option.type}>
<Handle
type='source'
position={Position.Right}
key={index}
id={option?.id}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: getAnchorPosition(outputAnchor.options, index)
}}
/>
</CustomWidthTooltip>
<div style={{ flex: 1 }}></div>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>{option.label}</Typography>
</Box>
</div>
)
})}
</div>
)}
{data.name === 'ifElseFunction' && outputAnchor.type === 'options' && outputAnchor.options && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
@@ -134,7 +183,7 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
{data.name !== 'ifElseFunction' &&
outputAnchor.type === 'options' &&
outputAnchor.options &&
outputAnchor.options.length > 0 && (
getAvailableOptions(outputAnchor.options).length > 0 && (
<>
<CustomWidthTooltip
placement='right'
@@ -161,7 +210,7 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
disabled={disabled}
disableClearable={true}
name={outputAnchor.name}
options={outputAnchor.options}
options={getAvailableOptions(outputAnchor.options)}
onSelect={(newValue) => {
setDropdownValue(newValue)
data.outputs[outputAnchor.name] = newValue
+165 -74
View File
@@ -35,7 +35,9 @@ import {
IconTrash,
IconX,
IconTool,
IconSquareFilled
IconSquareFilled,
IconDeviceSdCard,
IconCheck
} from '@tabler/icons-react'
import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png'
@@ -54,9 +56,9 @@ import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-componen
import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'
import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'
import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'
import './ChatMessage.css'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
import './audio-recording.css'
import './ChatMessage.css'
// api
import chatmessageApi from '@/api/chatmessage'
@@ -408,7 +410,16 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].agentReasoning = JSON.parse(agentReasoning)
allMessages[allMessages.length - 1].agentReasoning = agentReasoning
return allMessages
})
}
const updateLastMessageAction = (action) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].action = action
return allMessages
})
}
@@ -488,11 +499,22 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
handleSubmit(undefined, promptStarterInput)
}
const handleActionClick = async (elem, action) => {
setUserInput(elem.label)
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].action = null
return allMessages
})
handleSubmit(undefined, elem.label, action)
}
// Handle form submission
const handleSubmit = async (e, promptStarterInput) => {
const handleSubmit = async (e, selectedInput, action) => {
if (e) e.preventDefault()
if (!promptStarterInput && userInput.trim() === '') {
if (!selectedInput && userInput.trim() === '') {
const containsAudio = previews.filter((item) => item.type === 'audio').length > 0
if (!(previews.length >= 1 && containsAudio)) {
return
@@ -501,7 +523,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
let input = userInput
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
setLoading(true)
const urls = previews.map((item) => {
@@ -524,6 +546,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (urls && urls.length > 0) params.uploads = urls
if (leadEmail) params.leadEmail = leadEmail
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
if (action) params.action = action
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
@@ -566,6 +589,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
usedTools: data?.usedTools,
fileAnnotations: data?.fileAnnotations,
agentReasoning: data?.agentReasoning,
action: data?.action,
type: 'apiMessage',
feedback: null
}
@@ -638,6 +662,16 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}
}
const getAgentIcon = (nodeName, instructions) => {
if (nodeName) {
return `${baseURL}/api/v1/node-icon/${nodeName}`
} else if (instructions) {
return multiagent_supervisorPNG
} else {
return multiagent_workerPNG
}
}
// Get chatmessages successful
useEffect(() => {
if (getChatmessageApi.data?.length) {
@@ -654,6 +688,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
if (message.agentReasoning) obj.agentReasoning = JSON.parse(message.agentReasoning)
if (message.action) obj.action = JSON.parse(message.action)
if (message.fileUploads) {
obj.fileUploads = JSON.parse(message.fileUploads)
obj.fileUploads.forEach((file) => {
@@ -778,6 +813,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
socket.on('agentReasoning', updateLastMessageAgentReasoning)
socket.on('action', updateLastMessageAction)
socket.on('nextAgent', updateLastMessageNextAgent)
socket.on('abort', abortMessage)
@@ -919,6 +956,15 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
setIsLeadSaving(false)
}
const getInputDisabled = () => {
return (
loading ||
!chatflowid ||
(leadsConfig?.status && !isLeadSaved) ||
(messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0)
)
}
return (
<div onDragEnter={handleDrag}>
{isDragActive && (
@@ -981,31 +1027,6 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
width: '100%'
}}
>
{message.usedTools && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.usedTools.map((tool, index) => {
return tool ? (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={<IconTool size={15} />}
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
) : null
})}
</div>
)}
{message.fileUploads && message.fileUploads.length > 0 && (
<div
style={{
@@ -1117,11 +1138,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
height: '25px',
width: 'auto'
}}
src={
agent.instructions
? multiagent_supervisorPNG
: multiagent_workerPNG
}
src={getAgentIcon(agent.nodeName, agent.instructions)}
alt='agentPNG'
/>
</Box>
@@ -1152,6 +1169,26 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
})}
</div>
)}
{agent.state && Object.keys(agent.state).length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
<Chip
size='small'
label={'State'}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={<IconDeviceSdCard size={15} />}
onClick={() => onSourceDialogClick(agent.state, 'State')}
/>
</div>
)}
{agent.messages.length > 0 && (
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
@@ -1221,6 +1258,31 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
})}
</div>
)}
{message.usedTools && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.usedTools.map((tool, index) => {
return tool ? (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={<IconTool size={15} />}
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
) : null
})}
</div>
)}
<div className='markdownanswer'>
{message.type === 'leadCaptureMessage' &&
!getLocalStorageChatflow(chatflowid)?.lead &&
@@ -1417,6 +1479,64 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
})}
</div>
)}
{message.action && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
width: '100%',
gap: '8px'
}}
>
{(message.action.elements || []).map((elem, index) => {
return (
<>
{elem.type === 'approve-button' && elem.label === 'Yes' ? (
<Button
sx={{
width: 'max-content',
borderRadius: '20px',
background: customization.isDarkMode ? 'transparent' : 'white'
}}
variant='outlined'
color='success'
key={index}
startIcon={<IconCheck />}
onClick={() => handleActionClick(elem, message.action)}
>
{elem.label}
</Button>
) : elem.type === 'reject-button' && elem.label === 'No' ? (
<Button
sx={{
width: 'max-content',
borderRadius: '20px',
background: customization.isDarkMode ? 'transparent' : 'white'
}}
variant='outlined'
color='error'
key={index}
startIcon={<IconX />}
onClick={() => handleActionClick(elem, message.action)}
>
{elem.label}
</Button>
) : (
<Button
sx={{ width: 'max-content', borderRadius: '20px', background: 'white' }}
variant='outlined'
key={index}
onClick={() => handleActionClick(elem, message.action)}
>
{elem.label}
</Button>
)}
</>
)
})}
</div>
)}
</div>
</Box>
)
@@ -1546,7 +1666,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
// eslint-disable-next-line
autoFocus
sx={{ width: '100%' }}
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
disabled={getInputDisabled()}
onKeyDown={handleEnter}
id='userInput'
name='userInput'
@@ -1558,20 +1678,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
startAdornment={
isChatFlowAvailableForUploads && (
<InputAdornment position='start' sx={{ pl: 2 }}>
<IconButton
onClick={handleUploadClick}
type='button'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='start'
>
<IconButton onClick={handleUploadClick} type='button' disabled={getInputDisabled()} edge='start'>
<IconPhotoPlus
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
@@ -1584,29 +1693,19 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
<IconButton
onClick={() => onMicrophonePressed()}
type='button'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
disabled={getInputDisabled()}
edge='end'
>
<IconMicrophone
className={'start-recording-button'}
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
)}
{!isAgentCanvas && (
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
<IconButton type='submit' disabled={getInputDisabled()} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
@@ -1615,11 +1714,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
}
/>
)}
@@ -1630,14 +1725,10 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
<>
{!loading && (
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
<IconButton type='submit' disabled={getInputDisabled()} edge='end'>
<IconSend
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
getInputDisabled()
? '#9e9e9e'
: customization.isDarkMode
? 'white'
@@ -46,7 +46,9 @@ const MarketplaceCanvas = () => {
}, [flowData])
const onChatflowCopy = (flowData) => {
const isAgentCanvas = (flowData?.nodes || []).some((node) => node.data.category === 'Multi Agents')
const isAgentCanvas = (flowData?.nodes || []).some(
(node) => node.data.category === 'Multi Agents' || node.data.category === 'Sequential Agents'
)
const templateFlowData = JSON.stringify(flowData)
navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}`, { state: { templateFlowData } })
}
@@ -46,6 +46,9 @@ const HowToUseFunctionDialog = ({ show, onCancel }) => {
<li>
<code>$flow.input</code>
</li>
<li>
<code>$flow.state</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
+1 -1
View File
@@ -33,7 +33,7 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const exampleAPIFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Input Schema as variables. Ex: Property = userid, Variable = $userid
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input, $flow.state
* You can get custom variables: $vars.<variable-name>
* Must return a string value at the end of function
*/