Feature/Custom Function to Seq Agent (#3612)

* add custom function to seq agent

* add seqExecuteFlow node
This commit is contained in:
Henry Heng
2025-01-23 13:04:40 +00:00
committed by GitHub
parent 50a7339299
commit e26fc63be0
17 changed files with 36048 additions and 35393 deletions
@@ -205,7 +205,7 @@ class Agent_SeqAgents implements INode {
constructor() {
this.label = 'Agent'
this.name = 'seqAgent'
this.version = 4.0
this.version = 4.1
this.type = 'Agent'
this.icon = 'seqAgent.png'
this.category = 'Sequential Agents'
@@ -291,9 +291,11 @@ class Agent_SeqAgents implements INode {
optional: true
},
{
label: 'Start | Agent | Condition | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
@@ -96,7 +96,7 @@ class Condition_SeqAgents implements INode {
constructor() {
this.label = 'Condition'
this.name = 'seqCondition'
this.version = 2.0
this.version = 2.1
this.type = 'Condition'
this.icon = 'condition.svg'
this.category = 'Sequential Agents'
@@ -112,9 +112,11 @@ class Condition_SeqAgents implements INode {
placeholder: 'If X, then Y'
},
{
label: 'Start | Agent | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | LLMNode | ToolNode',
type: 'Start | Agent | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
@@ -151,7 +151,7 @@ class ConditionAgent_SeqAgents implements INode {
constructor() {
this.label = 'Condition Agent'
this.name = 'seqConditionAgent'
this.version = 3.0
this.version = 3.1
this.type = 'ConditionAgent'
this.icon = 'condition.svg'
this.category = 'Sequential Agents'
@@ -166,9 +166,11 @@ class ConditionAgent_SeqAgents implements INode {
placeholder: 'Condition Agent'
},
{
label: 'Start | Agent | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | LLMNode | ToolNode',
type: 'Start | Agent | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
@@ -0,0 +1,257 @@
import { NodeVM } from '@flowiseai/nodevm'
import { DataSource } from 'typeorm'
import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode, ISeqAgentsState } from '../../../src/Interface'
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'
import { customGet } from '../commonUtils'
const howToUseCode = `
1. Must return a string value at the end of function.
2. You can get default flow config, including the current "state":
- \`$flow.sessionId\`
- \`$flow.chatId\`
- \`$flow.chatflowId\`
- \`$flow.input\`
- \`$flow.state\`
3. You can get custom variables: \`$vars.<variable-name>\`
`
class CustomFunction_SeqAgents implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Custom JS Function'
this.name = 'seqCustomFunction'
this.version = 1.0
this.type = 'CustomFunction'
this.icon = 'customfunction.svg'
this.category = 'Sequential Agents'
this.description = `Execute custom javascript function`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Input Variables',
name: 'functionInputVariables',
description: 'Input variables can be used in the function with prefix $. For example: $var',
type: 'json',
optional: true,
acceptVariable: true,
list: true
},
{
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
label: 'Function Name',
name: 'functionName',
type: 'string',
placeholder: 'My Function'
},
{
label: 'Javascript Function',
name: 'javascriptFunction',
type: 'code',
hint: {
label: 'How to use',
value: howToUseCode
}
},
{
label: 'Return Value As',
name: 'returnValueAs',
type: 'options',
options: [
{ label: 'AI Message', name: 'aiMessage' },
{ label: 'Human Message', name: 'humanMessage' },
{
label: 'State Object',
name: 'stateObj',
description: "Return as state object, ex: { foo: bar }. This will update the custom state 'foo' to 'bar'"
}
],
default: 'aiMessage'
}
]
}
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const functionName = nodeData.inputs?.functionName as string
const javascriptFunction = nodeData.inputs?.javascriptFunction as string
const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
const returnValueAs = nodeData.inputs?.returnValueAs as string
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Custom function must have a predecessor!')
const executeFunc = async (state: ISeqAgentsState) => {
const variables = await getVars(appDataSource, databaseEntities, nodeData)
const flow = {
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
input,
state
}
let inputVars: ICommonObject = {}
if (functionInputVariablesRaw) {
try {
inputVars =
typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw)
} catch (exception) {
throw new Error('Invalid JSON in the Custom Function Input Variables: ' + exception)
}
}
// Some values might be a stringified JSON, parse it
for (const key in inputVars) {
let value = inputVars[key]
if (typeof value === 'string') {
value = handleEscapeCharacters(value, true)
if (value.startsWith('{') && value.endsWith('}')) {
try {
value = JSON.parse(value)
const nodeId = value.id || ''
if (nodeId) {
const messages = state.messages as unknown as BaseMessage[]
const content = messages.find((msg) => msg.additional_kwargs?.nodeId === nodeId)?.content
if (content) {
value = content
}
}
} catch (e) {
// ignore
}
}
if (value.startsWith('$flow.')) {
const variableValue = customGet(flow, value.replace('$flow.', ''))
if (variableValue) {
value = variableValue
}
} else if (value.startsWith('$vars')) {
value = customGet(flow, value.replace('$', ''))
}
inputVars[key] = value
}
}
let sandbox: any = {
$input: input,
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined
}
sandbox['$vars'] = prepareSandboxVars(variables)
sandbox['$flow'] = flow
if (Object.keys(inputVars).length) {
for (const item in inputVars) {
sandbox[`$${item}`] = inputVars[item]
}
}
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
: defaultAllowBuiltInDep
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
const deps = availableDependencies.concat(externalDeps)
const nodeVMOptions = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout: 10000
} as any
const vm = new NodeVM(nodeVMOptions)
try {
const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname)
if (returnValueAs === 'stateObj') {
if (typeof response !== 'object') {
throw new Error('Custom function must return an object!')
}
return {
...state,
...response
}
}
if (typeof response !== 'string') {
throw new Error('Custom function must return a string!')
}
if (returnValueAs === 'humanMessage') {
return {
messages: [
new HumanMessage({
content: response,
additional_kwargs: {
nodeId: nodeData.id
}
})
]
}
}
return {
messages: [
new AIMessage({
content: response,
additional_kwargs: {
nodeId: nodeData.id
}
})
]
}
} catch (e) {
throw new Error(e)
}
}
const startLLM = sequentialNodes[0].startLLM
const returnOutput: ISeqAgentNode = {
id: nodeData.id,
node: executeFunc,
name: functionName.toLowerCase().replace(/\s/g, '_').trim(),
label: functionName,
type: 'utilities',
output: 'CustomFunction',
llm: startLLM,
startLLM,
multiModalMessageContent: sequentialNodes[0]?.multiModalMessageContent,
predecessorAgents: sequentialNodes
}
return returnOutput
}
}
module.exports = { nodeClass: CustomFunction_SeqAgents }
@@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 27H7.23536C9.13552 27 10.7734 25.6632 11.1542 23.8016L14.3458 8.19842C14.7266 6.3368 16.3645 5 18.2646 5H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 11H18" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 17H17.8676C18.5701 17 19.2212 17.3686 19.5826 17.971L24.4174 26.029C24.7788 26.6314 25.4299 27 26.1324 27H27" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 17H26.1324C25.4299 17 24.7788 17.3686 24.4174 17.971L19.5826 26.029C19.2212 26.6314 18.5701 27 17.8676 27H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 816 B

@@ -18,7 +18,7 @@ class End_SeqAgents implements INode {
constructor() {
this.label = 'End'
this.name = 'seqEnd'
this.version = 2.0
this.version = 2.1
this.type = 'End'
this.icon = 'end.svg'
this.category = 'Sequential Agents'
@@ -27,9 +27,11 @@ class End_SeqAgents implements INode {
this.documentation = 'https://docs.flowiseai.com/using-flowise/agentflows/sequential-agents#id-10.-end-node'
this.inputs = [
{
label: 'Agent | Condition | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Agent | Condition | LLMNode | ToolNode'
type: 'Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow'
}
]
this.hideOutput = true
@@ -0,0 +1,339 @@
import { NodeVM } from '@flowiseai/nodevm'
import { DataSource } from 'typeorm'
import {
availableDependencies,
defaultAllowBuiltInDep,
getCredentialData,
getCredentialParam,
getVars,
prepareSandboxVars
} from '../../../src/utils'
import {
ICommonObject,
IDatabaseEntity,
INode,
INodeData,
INodeOptionsValue,
INodeParams,
ISeqAgentNode,
ISeqAgentsState
} from '../../../src/Interface'
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'
import { v4 as uuidv4 } from 'uuid'
class ExecuteFlow_SeqAgents implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
credential: INodeParams
constructor() {
this.label = 'Execute Flow'
this.name = 'seqExecuteFlow'
this.version = 1.0
this.type = 'ExecuteFlow'
this.icon = 'executeflow.svg'
this.category = 'Sequential Agents'
this.description = `Execute chatflow/agentflow and return final response`
this.baseClasses = [this.type]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['chatflowApi'],
optional: true
}
this.inputs = [
{
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
label: 'Name',
name: 'seqExecuteFlowName',
type: 'string'
},
{
label: 'Select Flow',
name: 'selectedFlow',
type: 'asyncOptions',
loadMethod: 'listFlows'
},
{
label: 'Input',
name: 'seqExecuteFlowInput',
type: 'options',
description: 'Select one of the following or enter custom input',
freeSolo: true,
loadPreviousNodes: true,
options: [
{
label: '{{ question }}',
name: 'userQuestion',
description: 'Use the user question from the chat as input.'
}
]
},
{
label: 'Override Config',
name: 'overrideConfig',
description: 'Override the config passed to the flow.',
type: 'json',
optional: true,
additionalParams: true
},
{
label: 'Base URL',
name: 'baseURL',
type: 'string',
description:
'Base URL to Flowise. By default, it is the URL of the incoming request. Useful when you need to execute flow through an alternative route.',
placeholder: 'http://localhost:3000',
optional: true,
additionalParams: true
},
{
label: 'Start new session per message',
name: 'startNewSession',
type: 'boolean',
description:
'Whether to continue the session or start a new one with each interaction. Useful for flows with memory if you want to avoid it.',
default: false,
optional: true,
additionalParams: true
},
{
label: 'Return Value As',
name: 'returnValueAs',
type: 'options',
options: [
{ label: 'AI Message', name: 'aiMessage' },
{ label: 'Human Message', name: 'humanMessage' },
{
label: 'State Object',
name: 'stateObj',
description: "Return as state object, ex: { foo: bar }. This will update the custom state 'foo' to 'bar'"
}
],
default: 'aiMessage'
}
]
}
//@ts-ignore
loadMethods = {
async listFlows(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
if (appDataSource === undefined || !appDataSource) {
return returnData
}
const chatflows = await appDataSource.getRepository(databaseEntities['ChatFlow']).find()
for (let i = 0; i < chatflows.length; i += 1) {
const data = {
label: chatflows[i].name,
name: chatflows[i].id
} as INodeOptionsValue
returnData.push(data)
}
return returnData
}
}
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const selectedFlowId = nodeData.inputs?.selectedFlow as string
const _seqExecuteFlowName = nodeData.inputs?.seqExecuteFlowName as string
if (!_seqExecuteFlowName) throw new Error('Execute Flow node name is required!')
const seqExecuteFlowName = _seqExecuteFlowName.toLowerCase().replace(/\s/g, '_').trim()
const startNewSession = nodeData.inputs?.startNewSession as boolean
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
const seqExecuteFlowInput = nodeData.inputs?.seqExecuteFlowInput as string
const overrideConfig =
typeof nodeData.inputs?.overrideConfig === 'string' &&
nodeData.inputs.overrideConfig.startsWith('{') &&
nodeData.inputs.overrideConfig.endsWith('}')
? JSON.parse(nodeData.inputs.overrideConfig)
: nodeData.inputs?.overrideConfig
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Execute Flow must have a predecessor!')
const baseURL = (nodeData.inputs?.baseURL as string) || (options.baseURL as string)
const returnValueAs = nodeData.inputs?.returnValueAs as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData)
if (selectedFlowId === options.chatflowid) throw new Error('Cannot call the same agentflow!')
let headers = {}
if (chatflowApiKey) headers = { Authorization: `Bearer ${chatflowApiKey}` }
const chatflowId = options.chatflowid
const sessionId = options.sessionId
const chatId = options.chatId
const executeFunc = async (state: ISeqAgentsState) => {
const variables = await getVars(appDataSource, databaseEntities, nodeData)
let flowInput = ''
if (seqExecuteFlowInput === 'userQuestion') {
flowInput = input
} else if (seqExecuteFlowInput && seqExecuteFlowInput.startsWith('{{') && seqExecuteFlowInput.endsWith('}}')) {
const nodeId = seqExecuteFlowInput.replace('{{', '').replace('}}', '').replace('$', '').trim()
const messageOutputs = ((state.messages as unknown as BaseMessage[]) ?? []).filter(
(message) => message.additional_kwargs && message.additional_kwargs?.nodeId === nodeId
)
const messageOutput = messageOutputs[messageOutputs.length - 1]
if (messageOutput) {
flowInput = JSON.stringify(messageOutput.content)
}
}
const flow = {
chatflowId,
sessionId,
chatId,
input: flowInput,
state
}
const body = {
question: flowInput,
chatId: startNewSession ? uuidv4() : chatId,
overrideConfig: {
sessionId: startNewSession ? uuidv4() : sessionId,
...(overrideConfig ?? {})
}
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}
let sandbox: ICommonObject = {
$input: flowInput,
$callOptions: options,
$callBody: body,
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined
}
sandbox['$vars'] = prepareSandboxVars(variables)
sandbox['$flow'] = flow
const code = `
const fetch = require('node-fetch');
const url = "${baseURL}/api/v1/prediction/${selectedFlowId}";
const body = $callBody;
const options = $callOptions;
try {
const response = await fetch(url, options);
const resp = await response.json();
return resp.text;
} catch (error) {
console.error(error);
return '';
}
`
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
: defaultAllowBuiltInDep
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
const deps = availableDependencies.concat(externalDeps)
const nodeVMOptions = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout: 10000
} as any
const vm = new NodeVM(nodeVMOptions)
try {
let response = await vm.run(`module.exports = async function() {${code}}()`, __dirname)
if (typeof response === 'object') {
response = JSON.stringify(response)
}
if (returnValueAs === 'humanMessage') {
return {
messages: [
new HumanMessage({
content: response,
additional_kwargs: {
nodeId: nodeData.id
}
})
]
}
}
return {
messages: [
new AIMessage({
content: response,
additional_kwargs: {
nodeId: nodeData.id
}
})
]
}
} catch (e) {
throw new Error(e)
}
}
const startLLM = sequentialNodes[0].startLLM
const returnOutput: ISeqAgentNode = {
id: nodeData.id,
node: executeFunc,
name: seqExecuteFlowName,
label: _seqExecuteFlowName,
type: 'utilities',
output: 'ExecuteFlow',
llm: startLLM,
startLLM,
multiModalMessageContent: sequentialNodes[0]?.multiModalMessageContent,
predecessorAgents: sequentialNodes
}
return returnOutput
}
}
module.exports = { nodeClass: ExecuteFlow_SeqAgents }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-schema"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 2h5v4h-5z" /><path d="M15 10h5v4h-5z" /><path d="M5 18h5v4h-5z" /><path d="M5 10h5v4h-5z" /><path d="M10 12h5" /><path d="M7.5 6v4" /><path d="M7.5 14v4" /></svg>

After

Width:  |  Height:  |  Size: 481 B

@@ -182,7 +182,7 @@ class LLMNode_SeqAgents implements INode {
constructor() {
this.label = 'LLM Node'
this.name = 'seqLLMNode'
this.version = 4.0
this.version = 4.1
this.type = 'LLMNode'
this.icon = 'llmNode.svg'
this.category = 'Sequential Agents'
@@ -261,9 +261,11 @@ class LLMNode_SeqAgents implements INode {
additionalParams: true
},
{
label: 'Start | Agent | Condition | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode',
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM, Tool Node, Custom Function, Execute Flow',
list: true
},
{
@@ -17,7 +17,7 @@ class Loop_SeqAgents implements INode {
constructor() {
this.label = 'Loop'
this.name = 'seqLoop'
this.version = 2.0
this.version = 2.1
this.type = 'Loop'
this.icon = 'loop.svg'
this.category = 'Sequential Agents'
@@ -26,9 +26,11 @@ class Loop_SeqAgents implements INode {
this.documentation = 'https://docs.flowiseai.com/using-flowise/agentflows/sequential-agents#id-9.-loop-node'
this.inputs = [
{
label: 'Agent | Condition | LLM | Tool Node',
label: 'Sequential Node',
name: 'sequentialNode',
type: 'Agent | Condition | LLMNode | ToolNode',
type: 'Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
description:
'Can be connected to one of the following nodes: Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
list: true
},
{
+3 -1
View File
@@ -94,6 +94,8 @@ export interface INodeParams {
tabIdentifier?: string
tabs?: Array<INodeParams>
refresh?: boolean
freeSolo?: boolean
loadPreviousNodes?: boolean
}
export interface INodeExecutionData {
@@ -183,7 +185,7 @@ export interface IMultiAgentNode {
checkpointMemory?: any
}
type SeqAgentType = 'agent' | 'condition' | 'end' | 'start' | 'tool' | 'state' | 'llm'
type SeqAgentType = 'agent' | 'condition' | 'end' | 'start' | 'tool' | 'state' | 'llm' | 'utilities'
export type ConversationHistorySelection = 'user_question' | 'last_message' | 'all_messages' | 'empty'
export interface ISeqAgentNode {
+2 -2
View File
@@ -904,8 +904,8 @@ const compileSeqAgentsGraph = async (params: SeqAgentsGraphParams) => {
const agentNode = reactFlowNodes.find((node) => node.id === agentNodeId)
if (!agentNode) continue
const eligibleSeqNodes = ['seqAgent', 'seqEnd', 'seqLoop', 'seqToolNode', 'seqLLMNode']
const nodesToAdd = ['seqAgent', 'seqToolNode', 'seqLLMNode']
const eligibleSeqNodes = ['seqAgent', 'seqEnd', 'seqLoop', 'seqToolNode', 'seqLLMNode', 'seqCustomFunction', 'seqExecuteFlow']
const nodesToAdd = ['seqAgent', 'seqToolNode', 'seqLLMNode', 'seqCustomFunction', 'seqExecuteFlow']
if (eligibleSeqNodes.includes(agentNode.data.name)) {
try {
@@ -58,6 +58,7 @@ export const AsyncDropdown = ({
onCreateNew,
credentialNames = [],
disabled = false,
freeSolo = false,
disableClearable = false
}) => {
const customization = useSelector((state) => state.customization)
@@ -114,6 +115,7 @@ export const AsyncDropdown = ({
<>
<Autocomplete
id={name}
freeSolo={freeSolo}
disabled={disabled}
disableClearable={disableClearable}
size='small'
@@ -176,6 +178,7 @@ AsyncDropdown.propTypes = {
onSelect: PropTypes.func,
onCreateNew: PropTypes.func,
disabled: PropTypes.bool,
freeSolo: PropTypes.bool,
credentialNames: PropTypes.array,
disableClearable: PropTypes.bool,
isCreateNewOption: PropTypes.bool
@@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({
}
})
export const Dropdown = ({ name, value, loading, options, onSelect, disabled = false, disableClearable = false }) => {
export const Dropdown = ({ name, value, loading, options, onSelect, disabled = false, freeSolo = false, disableClearable = false }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
@@ -29,6 +29,7 @@ export const Dropdown = ({ name, value, loading, options, onSelect, disabled = f
<Autocomplete
id={name}
disabled={disabled}
freeSolo={freeSolo}
disableClearable={disableClearable}
size='small'
loading={loading}
@@ -101,6 +102,7 @@ Dropdown.propTypes = {
value: PropTypes.string,
loading: PropTypes.bool,
options: PropTypes.array,
freeSolo: PropTypes.bool,
onSelect: PropTypes.func,
disabled: PropTypes.bool,
disableClearable: PropTypes.bool
+1 -1
View File
@@ -435,7 +435,7 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
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']
const excludeNodeNames = ['seqAgent', 'seqLLMNode', 'seqToolNode', 'seqCustomFunction', 'seqExecuteFlow']
if (excludeNodeNames.includes(parentNode.data.name)) {
parentNodes.push(parentNode)
}
@@ -319,6 +319,26 @@ const NodeInputHandler = ({
return colDef
}
const getDropdownOptions = (inputParam) => {
const preLoadOptions = []
if (inputParam.loadPreviousNodes) {
const nodes = getAvailableNodesForVariable(
reactFlowInstance?.getNodes() || [],
reactFlowInstance?.getEdges() || [],
data.id,
inputParam.id
)
for (const node of nodes) {
preLoadOptions.push({
name: `{{ ${node.data.id} }}`,
label: `{{ ${node.data.id} }}`,
description: `Output from ${node.data.id}`
})
}
}
return [...preLoadOptions, ...inputParam.options]
}
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}`])
@@ -515,6 +535,20 @@ const NodeInputHandler = ({
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{inputParam.hint && !isAdditionalParams && (
<IconButton
size='small'
sx={{
height: 25,
width: 25
}}
title={inputParam.hint.label}
color='secondary'
onClick={() => onInputHintDialogClicked(inputParam.hint)}
>
<IconBulb />
</IconButton>
)}
{inputParam.hint && isAdditionalParams && (
<Button
sx={{ p: 0, px: 2 }}
@@ -721,7 +755,8 @@ const NodeInputHandler = ({
<Dropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
options={getDropdownOptions(inputParam)}
freeSolo={inputParam.freeSolo}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
@@ -730,7 +765,7 @@ const NodeInputHandler = ({
<MultiDropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
options={getDropdownOptions(inputParam)}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
@@ -744,6 +779,7 @@ const NodeInputHandler = ({
name={inputParam.name}
nodeData={data}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
freeSolo={inputParam.freeSolo}
isCreateNewOption={EDITABLE_OPTIONS.includes(inputParam.name)}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
onCreateNew={() => addAsyncOption(inputParam.name)}
+35365 -35368
View File
File diff suppressed because it is too large Load Diff