Skip to content

Commit c870b4c

Browse files
authored
Merge pull request #387 from FlowiseAI/feature/CustomTool
Feature/Add CustomTool
2 parents 5261a81 + 4a63b68 commit c870b4c

File tree

29 files changed

+1575
-120
lines changed

29 files changed

+1575
-120
lines changed

packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
1+
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
22
import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents'
3-
import { Tool } from 'langchain/tools'
43
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
54
import { BaseLanguageModel } from 'langchain/base_language'
65
import { flatten } from 'lodash'
6+
import { BaseChatMemory, ChatMessageHistory } from 'langchain/memory'
7+
import { AIChatMessage, HumanChatMessage } from 'langchain/schema'
78

89
class OpenAIFunctionAgent_Agents implements INode {
910
label: string
@@ -30,30 +31,67 @@ class OpenAIFunctionAgent_Agents implements INode {
3031
type: 'Tool',
3132
list: true
3233
},
34+
{
35+
label: 'Memory',
36+
name: 'memory',
37+
type: 'BaseChatMemory'
38+
},
3339
{
3440
label: 'OpenAI Chat Model',
3541
name: 'model',
3642
description:
3743
'Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer <a target="_blank" href="https://platform.openai.com/docs/guides/gpt/function-calling">docs</a> for more info',
3844
type: 'BaseChatModel'
45+
},
46+
{
47+
label: 'System Message',
48+
name: 'systemMessage',
49+
type: 'string',
50+
rows: 4,
51+
optional: true,
52+
additionalParams: true
3953
}
4054
]
4155
}
4256

4357
async init(nodeData: INodeData): Promise<any> {
4458
const model = nodeData.inputs?.model as BaseLanguageModel
45-
let tools = nodeData.inputs?.tools as Tool[]
59+
const memory = nodeData.inputs?.memory as BaseChatMemory
60+
const systemMessage = nodeData.inputs?.systemMessage as string
61+
62+
let tools = nodeData.inputs?.tools
4663
tools = flatten(tools)
4764

4865
const executor = await initializeAgentExecutorWithOptions(tools, model, {
4966
agentType: 'openai-functions',
50-
verbose: process.env.DEBUG === 'true' ? true : false
67+
verbose: process.env.DEBUG === 'true' ? true : false,
68+
agentArgs: {
69+
prefix: systemMessage ?? `You are a helpful AI assistant.`
70+
}
5171
})
72+
if (memory) executor.memory = memory
73+
5274
return executor
5375
}
5476

5577
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
5678
const executor = nodeData.instance as AgentExecutor
79+
const memory = nodeData.inputs?.memory as BaseChatMemory
80+
81+
if (options && options.chatHistory) {
82+
const chatHistory = []
83+
const histories: IMessage[] = options.chatHistory
84+
85+
for (const message of histories) {
86+
if (message.type === 'apiMessage') {
87+
chatHistory.push(new AIChatMessage(message.message))
88+
} else if (message.type === 'userMessage') {
89+
chatHistory.push(new HumanChatMessage(message.message))
90+
}
91+
}
92+
memory.chatHistory = new ChatMessageHistory(chatHistory)
93+
executor.memory = memory
94+
}
5795

5896
if (options.socketIO && options.socketIOClientId) {
5997
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
2+
import { getBaseClasses } from '../../../src/utils'
3+
import { DynamicStructuredTool } from './core'
4+
import { z } from 'zod'
5+
import { DataSource } from 'typeorm'
6+
7+
class CustomTool_Tools implements INode {
8+
label: string
9+
name: string
10+
description: string
11+
type: string
12+
icon: string
13+
category: string
14+
baseClasses: string[]
15+
inputs: INodeParams[]
16+
17+
constructor() {
18+
this.label = 'Custom Tool'
19+
this.name = 'customTool'
20+
this.type = 'CustomTool'
21+
this.icon = 'customtool.svg'
22+
this.category = 'Tools'
23+
this.description = `Use custom tool you've created in Flowise within chatflow`
24+
this.inputs = [
25+
{
26+
label: 'Select Tool',
27+
name: 'selectedTool',
28+
type: 'asyncOptions',
29+
loadMethod: 'listTools'
30+
}
31+
]
32+
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(DynamicStructuredTool)]
33+
}
34+
35+
//@ts-ignore
36+
loadMethods = {
37+
async listTools(nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
38+
const returnData: INodeOptionsValue[] = []
39+
40+
const appDataSource = options.appDataSource as DataSource
41+
const databaseEntities = options.databaseEntities as IDatabaseEntity
42+
43+
if (appDataSource === undefined || !appDataSource) {
44+
return returnData
45+
}
46+
47+
const tools = await appDataSource.getRepository(databaseEntities['Tool']).find()
48+
49+
for (let i = 0; i < tools.length; i += 1) {
50+
const data = {
51+
label: tools[i].name,
52+
name: tools[i].id,
53+
description: tools[i].description
54+
} as INodeOptionsValue
55+
returnData.push(data)
56+
}
57+
return returnData
58+
}
59+
}
60+
61+
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
62+
const selectedToolId = nodeData.inputs?.selectedTool as string
63+
64+
const appDataSource = options.appDataSource as DataSource
65+
const databaseEntities = options.databaseEntities as IDatabaseEntity
66+
67+
try {
68+
const tool = await appDataSource.getRepository(databaseEntities['Tool']).findOneBy({
69+
id: selectedToolId
70+
})
71+
72+
if (!tool) throw new Error(`Tool ${selectedToolId} not found`)
73+
const obj = {
74+
name: tool.name,
75+
description: tool.description,
76+
schema: z.object(convertSchemaToZod(tool.schema)),
77+
code: tool.func
78+
}
79+
return new DynamicStructuredTool(obj)
80+
} catch (e) {
81+
throw new Error(e)
82+
}
83+
}
84+
}
85+
86+
const convertSchemaToZod = (schema: string) => {
87+
try {
88+
const parsedSchema = JSON.parse(schema)
89+
const zodObj: any = {}
90+
for (const sch of parsedSchema) {
91+
if (sch.type === 'string') {
92+
if (sch.required) z.string({ required_error: `${sch.property} required` }).describe(sch.description)
93+
zodObj[sch.property] = z.string().describe(sch.description)
94+
} else if (sch.type === 'number') {
95+
if (sch.required) z.number({ required_error: `${sch.property} required` }).describe(sch.description)
96+
zodObj[sch.property] = z.number().describe(sch.description)
97+
} else if (sch.type === 'boolean') {
98+
if (sch.required) z.boolean({ required_error: `${sch.property} required` }).describe(sch.description)
99+
zodObj[sch.property] = z.boolean().describe(sch.description)
100+
}
101+
}
102+
return zodObj
103+
} catch (e) {
104+
throw new Error(e)
105+
}
106+
}
107+
108+
module.exports = { nodeClass: CustomTool_Tools }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { z } from 'zod'
2+
import { CallbackManagerForToolRun } from 'langchain/callbacks'
3+
import { StructuredTool, ToolParams } from 'langchain/tools'
4+
import { NodeVM } from 'vm2'
5+
import { availableDependencies } from '../../../src/utils'
6+
7+
export interface BaseDynamicToolInput extends ToolParams {
8+
name: string
9+
description: string
10+
code: string
11+
returnDirect?: boolean
12+
}
13+
14+
export interface DynamicStructuredToolInput<
15+
// eslint-disable-next-line
16+
T extends z.ZodObject<any, any, any, any> = z.ZodObject<any, any, any, any>
17+
> extends BaseDynamicToolInput {
18+
func?: (input: z.infer<T>, runManager?: CallbackManagerForToolRun) => Promise<string>
19+
schema: T
20+
}
21+
22+
export class DynamicStructuredTool<
23+
// eslint-disable-next-line
24+
T extends z.ZodObject<any, any, any, any> = z.ZodObject<any, any, any, any>
25+
> extends StructuredTool {
26+
name: string
27+
28+
description: string
29+
30+
code: string
31+
32+
func: DynamicStructuredToolInput['func']
33+
34+
schema: T
35+
36+
constructor(fields: DynamicStructuredToolInput<T>) {
37+
super(fields)
38+
this.name = fields.name
39+
this.description = fields.description
40+
this.code = fields.code
41+
this.func = fields.func
42+
this.returnDirect = fields.returnDirect ?? this.returnDirect
43+
this.schema = fields.schema
44+
}
45+
46+
protected async _call(arg: z.output<T>): Promise<string> {
47+
let sandbox: any = {}
48+
if (typeof arg === 'object' && Object.keys(arg).length) {
49+
for (const item in arg) {
50+
sandbox[`$${item}`] = arg[item]
51+
}
52+
}
53+
54+
const options = {
55+
console: 'inherit',
56+
sandbox,
57+
require: {
58+
external: false as boolean | { modules: string[] },
59+
builtin: ['*']
60+
}
61+
} as any
62+
63+
const external = JSON.stringify(availableDependencies)
64+
if (external) {
65+
const deps = JSON.parse(external)
66+
if (deps && deps.length) {
67+
options.require.external = {
68+
modules: deps
69+
}
70+
}
71+
}
72+
73+
const vm = new NodeVM(options)
74+
const response = await vm.run(`module.exports = async function() {${this.code}}()`, __dirname)
75+
76+
return response
77+
}
78+
}
Loading

packages/components/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"form-data": "^4.0.0",
3434
"graphql": "^16.6.0",
3535
"html-to-text": "^9.0.5",
36-
"langchain": "^0.0.94",
36+
"langchain": "^0.0.96",
3737
"linkifyjs": "^4.1.1",
3838
"mammoth": "^1.5.1",
3939
"moment": "^2.29.3",
@@ -43,6 +43,7 @@
4343
"playwright": "^1.35.0",
4444
"puppeteer": "^20.7.1",
4545
"srt-parser-2": "^1.2.3",
46+
"vm2": "^3.9.19",
4647
"weaviate-ts-client": "^1.1.0",
4748
"ws": "^8.9.0"
4849
},

packages/components/src/Interface.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22
* Types
33
*/
44

5-
export type NodeParamsType = 'options' | 'string' | 'number' | 'boolean' | 'password' | 'json' | 'code' | 'date' | 'file' | 'folder'
5+
export type NodeParamsType =
6+
| 'asyncOptions'
7+
| 'options'
8+
| 'string'
9+
| 'number'
10+
| 'boolean'
11+
| 'password'
12+
| 'json'
13+
| 'code'
14+
| 'date'
15+
| 'file'
16+
| 'folder'
617

718
export type CommonType = string | number | boolean | undefined | null
819

@@ -16,6 +27,10 @@ export interface ICommonObject {
1627
[key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[]
1728
}
1829

30+
export type IDatabaseEntity = {
31+
[key: string]: any
32+
}
33+
1934
export interface IAttachment {
2035
content: string
2136
contentType: string
@@ -50,6 +65,7 @@ export interface INodeParams {
5065
placeholder?: string
5166
fileType?: string
5267
additionalParams?: boolean
68+
loadMethod?: string
5369
}
5470

5571
export interface INodeExecutionData {
@@ -74,6 +90,9 @@ export interface INodeProperties {
7490
export interface INode extends INodeProperties {
7591
inputs?: INodeParams[]
7692
output?: INodeOutputsValue[]
93+
loadMethods?: {
94+
[key: string]: (nodeData: INodeData, options?: ICommonObject) => Promise<INodeOptionsValue[]>
95+
}
7796
init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<any>
7897
run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<string | ICommonObject>
7998
}
@@ -83,6 +102,7 @@ export interface INodeData extends INodeProperties {
83102
inputs?: ICommonObject
84103
outputs?: ICommonObject
85104
instance?: any
105+
loadMethod?: string // method to load async options
86106
}
87107

88108
export interface IMessage {

0 commit comments

Comments
 (0)