250 lines
9.9 KiB
TypeScript
250 lines
9.9 KiB
TypeScript
import { START } from '@langchain/langgraph'
|
|
import { NodeVM } from '@flowiseai/nodevm'
|
|
import { DataSource } from 'typeorm'
|
|
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface'
|
|
import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils'
|
|
|
|
const defaultFunc = `{
|
|
aggregate: {
|
|
value: (x, y) => x.concat(y), // here we append the new message to the existing messages
|
|
default: () => []
|
|
}
|
|
}`
|
|
|
|
const howToUse = `
|
|
Specify the Key, Operation Type, and Default Value for the state object. The Operation Type can be either "Replace" or "Append".
|
|
|
|
**Replace**
|
|
- Replace the existing value with the new value.
|
|
- If the new value is null, the existing value will be retained.
|
|
|
|
**Append**
|
|
- Append the new value to the existing value.
|
|
- Default value can be empty or an array. Ex: ["a", "b"]
|
|
- Final value is an array.
|
|
`
|
|
const TAB_IDENTIFIER = 'selectedStateTab'
|
|
|
|
class State_SeqAgents implements INode {
|
|
label: string
|
|
name: string
|
|
version: number
|
|
description: string
|
|
type: string
|
|
icon: string
|
|
category: string
|
|
baseClasses: string[]
|
|
documentation?: string
|
|
credential: INodeParams
|
|
inputs: INodeParams[]
|
|
|
|
constructor() {
|
|
this.label = 'State'
|
|
this.name = 'seqState'
|
|
this.version = 2.0
|
|
this.type = 'State'
|
|
this.icon = 'state.svg'
|
|
this.category = 'Sequential Agents'
|
|
this.description = 'A centralized state object, updated by nodes in the graph, passing from one node to another'
|
|
this.baseClasses = [this.type]
|
|
this.documentation = 'https://docs.flowiseai.com/using-flowise/agentflows/sequential-agents#id-3.-state-node'
|
|
this.inputs = [
|
|
{
|
|
label: 'Custom State',
|
|
name: 'stateMemory',
|
|
type: 'tabs',
|
|
tabIdentifier: TAB_IDENTIFIER,
|
|
additionalParams: true,
|
|
default: 'stateMemoryUI',
|
|
tabs: [
|
|
{
|
|
label: 'Custom State (Table)',
|
|
name: 'stateMemoryUI',
|
|
type: 'datagrid',
|
|
description:
|
|
'Structure for state. By default, state contains "messages" that got updated with each message sent and received.',
|
|
hint: {
|
|
label: 'How to use',
|
|
value: howToUse
|
|
},
|
|
datagrid: [
|
|
{ field: 'key', headerName: 'Key', editable: true },
|
|
{
|
|
field: 'type',
|
|
headerName: 'Operation',
|
|
type: 'singleSelect',
|
|
valueOptions: ['Replace', 'Append'],
|
|
editable: true
|
|
},
|
|
{ field: 'defaultValue', headerName: 'Default Value', flex: 1, editable: true }
|
|
],
|
|
optional: true,
|
|
additionalParams: true
|
|
},
|
|
{
|
|
label: 'Custom State (Code)',
|
|
name: 'stateMemoryCode',
|
|
type: 'code',
|
|
description: `JSON object representing the state`,
|
|
hideCodeExecute: true,
|
|
codeExample: defaultFunc,
|
|
optional: true,
|
|
additionalParams: true
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
|
|
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
|
const stateMemoryUI = nodeData.inputs?.stateMemoryUI as string
|
|
const stateMemoryCode = nodeData.inputs?.stateMemoryCode as string
|
|
const appDataSource = options.appDataSource as DataSource
|
|
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
|
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'stateMemoryUI'
|
|
const stateMemory = nodeData.inputs?.stateMemory as string
|
|
|
|
if (stateMemory && stateMemory !== 'stateMemoryUI' && stateMemory !== 'stateMemoryCode') {
|
|
try {
|
|
const parsedSchemaFromUI = typeof stateMemoryUI === 'string' ? JSON.parse(stateMemoryUI) : stateMemoryUI
|
|
const parsedSchema = typeof stateMemory === 'string' ? JSON.parse(stateMemory) : stateMemory
|
|
const combinedMemorySchema = [...parsedSchemaFromUI, ...parsedSchema]
|
|
const obj: ICommonObject = {}
|
|
for (const sch of combinedMemorySchema) {
|
|
const key = sch.Key ?? sch.key
|
|
if (!key) throw new Error(`Key is required`)
|
|
const type = sch.Operation ?? sch.type
|
|
const defaultValue = sch['Default Value'] ?? sch.defaultValue
|
|
|
|
if (type === 'Append') {
|
|
obj[key] = {
|
|
value: (x: any, y: any) => (Array.isArray(y) ? x.concat(y) : x.concat([y])),
|
|
default: () => (defaultValue ? JSON.parse(defaultValue) : [])
|
|
}
|
|
} else {
|
|
obj[key] = {
|
|
value: (x: any, y: any) => y ?? x,
|
|
default: () => defaultValue
|
|
}
|
|
}
|
|
}
|
|
const returnOutput: ISeqAgentNode = {
|
|
id: nodeData.id,
|
|
node: obj,
|
|
name: 'state',
|
|
label: 'state',
|
|
type: 'state',
|
|
output: START
|
|
}
|
|
return returnOutput
|
|
} catch (e) {
|
|
throw new Error(e)
|
|
}
|
|
}
|
|
|
|
if (!stateMemoryUI && !stateMemoryCode) {
|
|
const returnOutput: ISeqAgentNode = {
|
|
id: nodeData.id,
|
|
node: {},
|
|
name: 'state',
|
|
label: 'state',
|
|
type: 'state',
|
|
output: START
|
|
}
|
|
return returnOutput
|
|
}
|
|
|
|
if (selectedTab === 'stateMemoryUI' && stateMemoryUI) {
|
|
try {
|
|
const parsedSchema = typeof stateMemoryUI === 'string' ? JSON.parse(stateMemoryUI) : stateMemoryUI
|
|
const obj: ICommonObject = {}
|
|
for (const sch of parsedSchema) {
|
|
const key = sch.key
|
|
if (!key) throw new Error(`Key is required`)
|
|
const type = sch.type
|
|
const defaultValue = sch.defaultValue
|
|
|
|
if (type === 'Append') {
|
|
obj[key] = {
|
|
value: (x: any, y: any) => (Array.isArray(y) ? x.concat(y) : x.concat([y])),
|
|
default: () => (defaultValue ? JSON.parse(defaultValue) : [])
|
|
}
|
|
} else {
|
|
obj[key] = {
|
|
value: (x: any, y: any) => y ?? x,
|
|
default: () => defaultValue
|
|
}
|
|
}
|
|
}
|
|
const returnOutput: ISeqAgentNode = {
|
|
id: nodeData.id,
|
|
node: obj,
|
|
name: 'state',
|
|
label: 'state',
|
|
type: 'state',
|
|
output: START
|
|
}
|
|
return returnOutput
|
|
} catch (e) {
|
|
throw new Error(e)
|
|
}
|
|
} else if (selectedTab === 'stateMemoryCode' && stateMemoryCode) {
|
|
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
|
const flow = {
|
|
chatflowId: options.chatflowid,
|
|
sessionId: options.sessionId,
|
|
chatId: options.chatId,
|
|
input
|
|
}
|
|
|
|
let sandbox: any = {
|
|
util: undefined,
|
|
Symbol: undefined,
|
|
child_process: undefined,
|
|
fs: undefined,
|
|
process: undefined
|
|
}
|
|
sandbox['$vars'] = prepareSandboxVars(variables)
|
|
sandbox['$flow'] = flow
|
|
|
|
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() {return ${stateMemoryCode}}()`, __dirname)
|
|
if (typeof response !== 'object') throw new Error('State must be an object')
|
|
const returnOutput: ISeqAgentNode = {
|
|
id: nodeData.id,
|
|
node: response,
|
|
name: 'state',
|
|
label: 'state',
|
|
type: 'state',
|
|
output: START
|
|
}
|
|
return returnOutput
|
|
} catch (e) {
|
|
throw new Error(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { nodeClass: State_SeqAgents }
|