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
},
{