Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Custom Function to Seq Agent #3612

Merged
merged 3 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/components/nodes/sequentialagents/Agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions packages/components/nodes/sequentialagents/End/End.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
Loading
Loading