Skip to content

Commit 86aaa67

Browse files
HenryHengZJYshayy
authored andcommitted
Feature/Custom Function to Seq Agent (FlowiseAI#3612)
* add custom function to seq agent * add seqExecuteFlow node
1 parent 7a42be5 commit 86aaa67

File tree

17 files changed

+35869
-35214
lines changed

17 files changed

+35869
-35214
lines changed

packages/components/nodes/sequentialagents/Agent/Agent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ class Agent_SeqAgents implements INode {
205205
constructor() {
206206
this.label = 'Agent'
207207
this.name = 'seqAgent'
208-
this.version = 4.0
208+
this.version = 4.1
209209
this.type = 'Agent'
210210
this.icon = 'seqAgent.png'
211211
this.category = 'Sequential Agents'
@@ -291,9 +291,11 @@ class Agent_SeqAgents implements INode {
291291
optional: true
292292
},
293293
{
294-
label: 'Start | Agent | Condition | LLM | Tool Node',
294+
label: 'Sequential Node',
295295
name: 'sequentialNode',
296-
type: 'Start | Agent | Condition | LLMNode | ToolNode',
296+
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
297+
description:
298+
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
297299
list: true
298300
},
299301
{

packages/components/nodes/sequentialagents/Condition/Condition.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class Condition_SeqAgents implements INode {
9696
constructor() {
9797
this.label = 'Condition'
9898
this.name = 'seqCondition'
99-
this.version = 2.0
99+
this.version = 2.1
100100
this.type = 'Condition'
101101
this.icon = 'condition.svg'
102102
this.category = 'Sequential Agents'
@@ -112,9 +112,11 @@ class Condition_SeqAgents implements INode {
112112
placeholder: 'If X, then Y'
113113
},
114114
{
115-
label: 'Start | Agent | LLM | Tool Node',
115+
label: 'Sequential Node',
116116
name: 'sequentialNode',
117-
type: 'Start | Agent | LLMNode | ToolNode',
117+
type: 'Start | Agent | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
118+
description:
119+
'Can be connected to one of the following nodes: Start, Agent, LLM Node, Tool Node, Custom Function, Execute Flow',
118120
list: true
119121
},
120122
{

packages/components/nodes/sequentialagents/ConditionAgent/ConditionAgent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class ConditionAgent_SeqAgents implements INode {
151151
constructor() {
152152
this.label = 'Condition Agent'
153153
this.name = 'seqConditionAgent'
154-
this.version = 3.0
154+
this.version = 3.1
155155
this.type = 'ConditionAgent'
156156
this.icon = 'condition.svg'
157157
this.category = 'Sequential Agents'
@@ -166,9 +166,11 @@ class ConditionAgent_SeqAgents implements INode {
166166
placeholder: 'Condition Agent'
167167
},
168168
{
169-
label: 'Start | Agent | LLM | Tool Node',
169+
label: 'Sequential Node',
170170
name: 'sequentialNode',
171-
type: 'Start | Agent | LLMNode | ToolNode',
171+
type: 'Start | Agent | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
172+
description:
173+
'Can be connected to one of the following nodes: Start, Agent, LLM Node, Tool Node, Custom Function, Execute Flow',
172174
list: true
173175
},
174176
{
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { NodeVM } from '@flowiseai/nodevm'
2+
import { DataSource } from 'typeorm'
3+
import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils'
4+
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode, ISeqAgentsState } from '../../../src/Interface'
5+
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'
6+
import { customGet } from '../commonUtils'
7+
8+
const howToUseCode = `
9+
1. Must return a string value at the end of function.
10+
11+
2. You can get default flow config, including the current "state":
12+
- \`$flow.sessionId\`
13+
- \`$flow.chatId\`
14+
- \`$flow.chatflowId\`
15+
- \`$flow.input\`
16+
- \`$flow.state\`
17+
18+
3. You can get custom variables: \`$vars.<variable-name>\`
19+
20+
`
21+
22+
class CustomFunction_SeqAgents implements INode {
23+
label: string
24+
name: string
25+
version: number
26+
description: string
27+
type: string
28+
icon: string
29+
category: string
30+
baseClasses: string[]
31+
inputs: INodeParams[]
32+
33+
constructor() {
34+
this.label = 'Custom JS Function'
35+
this.name = 'seqCustomFunction'
36+
this.version = 1.0
37+
this.type = 'CustomFunction'
38+
this.icon = 'customfunction.svg'
39+
this.category = 'Sequential Agents'
40+
this.description = `Execute custom javascript function`
41+
this.baseClasses = [this.type]
42+
this.inputs = [
43+
{
44+
label: 'Input Variables',
45+
name: 'functionInputVariables',
46+
description: 'Input variables can be used in the function with prefix $. For example: $var',
47+
type: 'json',
48+
optional: true,
49+
acceptVariable: true,
50+
list: true
51+
},
52+
{
53+
label: 'Sequential Node',
54+
name: 'sequentialNode',
55+
type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
56+
description:
57+
'Can be connected to one of the following nodes: Start, Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow',
58+
list: true
59+
},
60+
{
61+
label: 'Function Name',
62+
name: 'functionName',
63+
type: 'string',
64+
placeholder: 'My Function'
65+
},
66+
{
67+
label: 'Javascript Function',
68+
name: 'javascriptFunction',
69+
type: 'code',
70+
hint: {
71+
label: 'How to use',
72+
value: howToUseCode
73+
}
74+
},
75+
{
76+
label: 'Return Value As',
77+
name: 'returnValueAs',
78+
type: 'options',
79+
options: [
80+
{ label: 'AI Message', name: 'aiMessage' },
81+
{ label: 'Human Message', name: 'humanMessage' },
82+
{
83+
label: 'State Object',
84+
name: 'stateObj',
85+
description: "Return as state object, ex: { foo: bar }. This will update the custom state 'foo' to 'bar'"
86+
}
87+
],
88+
default: 'aiMessage'
89+
}
90+
]
91+
}
92+
93+
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
94+
const functionName = nodeData.inputs?.functionName as string
95+
const javascriptFunction = nodeData.inputs?.javascriptFunction as string
96+
const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables
97+
const appDataSource = options.appDataSource as DataSource
98+
const databaseEntities = options.databaseEntities as IDatabaseEntity
99+
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
100+
const returnValueAs = nodeData.inputs?.returnValueAs as string
101+
102+
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Custom function must have a predecessor!')
103+
104+
const executeFunc = async (state: ISeqAgentsState) => {
105+
const variables = await getVars(appDataSource, databaseEntities, nodeData)
106+
const flow = {
107+
chatflowId: options.chatflowid,
108+
sessionId: options.sessionId,
109+
chatId: options.chatId,
110+
input,
111+
state
112+
}
113+
114+
let inputVars: ICommonObject = {}
115+
if (functionInputVariablesRaw) {
116+
try {
117+
inputVars =
118+
typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw)
119+
} catch (exception) {
120+
throw new Error('Invalid JSON in the Custom Function Input Variables: ' + exception)
121+
}
122+
}
123+
124+
// Some values might be a stringified JSON, parse it
125+
for (const key in inputVars) {
126+
let value = inputVars[key]
127+
if (typeof value === 'string') {
128+
value = handleEscapeCharacters(value, true)
129+
if (value.startsWith('{') && value.endsWith('}')) {
130+
try {
131+
value = JSON.parse(value)
132+
const nodeId = value.id || ''
133+
if (nodeId) {
134+
const messages = state.messages as unknown as BaseMessage[]
135+
const content = messages.find((msg) => msg.additional_kwargs?.nodeId === nodeId)?.content
136+
if (content) {
137+
value = content
138+
}
139+
}
140+
} catch (e) {
141+
// ignore
142+
}
143+
}
144+
145+
if (value.startsWith('$flow.')) {
146+
const variableValue = customGet(flow, value.replace('$flow.', ''))
147+
if (variableValue) {
148+
value = variableValue
149+
}
150+
} else if (value.startsWith('$vars')) {
151+
value = customGet(flow, value.replace('$', ''))
152+
}
153+
inputVars[key] = value
154+
}
155+
}
156+
157+
let sandbox: any = {
158+
$input: input,
159+
util: undefined,
160+
Symbol: undefined,
161+
child_process: undefined,
162+
fs: undefined,
163+
process: undefined
164+
}
165+
sandbox['$vars'] = prepareSandboxVars(variables)
166+
sandbox['$flow'] = flow
167+
168+
if (Object.keys(inputVars).length) {
169+
for (const item in inputVars) {
170+
sandbox[`$${item}`] = inputVars[item]
171+
}
172+
}
173+
174+
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
175+
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
176+
: defaultAllowBuiltInDep
177+
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
178+
const deps = availableDependencies.concat(externalDeps)
179+
180+
const nodeVMOptions = {
181+
console: 'inherit',
182+
sandbox,
183+
require: {
184+
external: { modules: deps },
185+
builtin: builtinDeps
186+
},
187+
eval: false,
188+
wasm: false,
189+
timeout: 10000
190+
} as any
191+
192+
const vm = new NodeVM(nodeVMOptions)
193+
try {
194+
const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname)
195+
196+
if (returnValueAs === 'stateObj') {
197+
if (typeof response !== 'object') {
198+
throw new Error('Custom function must return an object!')
199+
}
200+
return {
201+
...state,
202+
...response
203+
}
204+
}
205+
206+
if (typeof response !== 'string') {
207+
throw new Error('Custom function must return a string!')
208+
}
209+
210+
if (returnValueAs === 'humanMessage') {
211+
return {
212+
messages: [
213+
new HumanMessage({
214+
content: response,
215+
additional_kwargs: {
216+
nodeId: nodeData.id
217+
}
218+
})
219+
]
220+
}
221+
}
222+
223+
return {
224+
messages: [
225+
new AIMessage({
226+
content: response,
227+
additional_kwargs: {
228+
nodeId: nodeData.id
229+
}
230+
})
231+
]
232+
}
233+
} catch (e) {
234+
throw new Error(e)
235+
}
236+
}
237+
238+
const startLLM = sequentialNodes[0].startLLM
239+
240+
const returnOutput: ISeqAgentNode = {
241+
id: nodeData.id,
242+
node: executeFunc,
243+
name: functionName.toLowerCase().replace(/\s/g, '_').trim(),
244+
label: functionName,
245+
type: 'utilities',
246+
output: 'CustomFunction',
247+
llm: startLLM,
248+
startLLM,
249+
multiModalMessageContent: sequentialNodes[0]?.multiModalMessageContent,
250+
predecessorAgents: sequentialNodes
251+
}
252+
253+
return returnOutput
254+
}
255+
}
256+
257+
module.exports = { nodeClass: CustomFunction_SeqAgents }
Lines changed: 6 additions & 0 deletions
Loading

packages/components/nodes/sequentialagents/End/End.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class End_SeqAgents implements INode {
1818
constructor() {
1919
this.label = 'End'
2020
this.name = 'seqEnd'
21-
this.version = 2.0
21+
this.version = 2.1
2222
this.type = 'End'
2323
this.icon = 'end.svg'
2424
this.category = 'Sequential Agents'
@@ -27,9 +27,11 @@ class End_SeqAgents implements INode {
2727
this.documentation = 'https://docs.flowiseai.com/using-flowise/agentflows/sequential-agents#id-10.-end-node'
2828
this.inputs = [
2929
{
30-
label: 'Agent | Condition | LLM | Tool Node',
30+
label: 'Sequential Node',
3131
name: 'sequentialNode',
32-
type: 'Agent | Condition | LLMNode | ToolNode'
32+
type: 'Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow',
33+
description:
34+
'Can be connected to one of the following nodes: Agent, Condition, LLM Node, Tool Node, Custom Function, Execute Flow'
3335
}
3436
]
3537
this.hideOutput = true

0 commit comments

Comments
 (0)