From 70da39629c84328ad864075011f8ba119c1645f2 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 21 Jun 2023 18:31:53 +0100 Subject: [PATCH 1/3] add custom tool --- .../OpenAIFunctionAgent.ts | 32 +- .../nodes/tools/ChainTool/chaintool.svg | 8 +- .../nodes/tools/CustomTool/CustomTool.ts | 108 +++++ .../components/nodes/tools/CustomTool/core.ts | 78 +++ .../nodes/tools/CustomTool/customtool.svg | 4 + packages/components/package.json | 3 +- packages/components/src/Interface.ts | 22 +- packages/components/src/utils.ts | 31 +- packages/server/src/ChildProcess.ts | 26 +- packages/server/src/DataSource.ts | 3 +- packages/server/src/Interface.ts | 11 + packages/server/src/entity/Tool.ts | 30 ++ packages/server/src/index.ts | 87 +++- packages/server/src/utils/index.ts | 14 +- packages/ui/package.json | 1 + packages/ui/src/api/tools.js | 19 + packages/ui/src/assets/images/tools_empty.svg | 1 + packages/ui/src/menu-items/dashboard.js | 12 +- packages/ui/src/routes/MainRoutes.js | 7 + .../ui/src/ui-component/cards/ItemCard.js | 46 +- .../ui-component/dropdown/AsyncDropdown.js | 147 ++++++ .../src/ui-component/editor/DarkCodeEditor.js | 1 + .../ui-component/editor/LightCodeEditor.js | 1 + packages/ui/src/ui-component/grid/Grid.js | 37 ++ packages/ui/src/utils/genericHelper.js | 21 +- .../ui/src/views/canvas/NodeInputHandler.js | 79 ++- packages/ui/src/views/tools/ToolDialog.js | 448 ++++++++++++++++++ packages/ui/src/views/tools/index.js | 112 +++++ 28 files changed, 1346 insertions(+), 43 deletions(-) create mode 100644 packages/components/nodes/tools/CustomTool/CustomTool.ts create mode 100644 packages/components/nodes/tools/CustomTool/core.ts create mode 100644 packages/components/nodes/tools/CustomTool/customtool.svg create mode 100644 packages/server/src/entity/Tool.ts create mode 100644 packages/ui/src/api/tools.js create mode 100644 packages/ui/src/assets/images/tools_empty.svg create mode 100644 packages/ui/src/ui-component/dropdown/AsyncDropdown.js create mode 100644 packages/ui/src/ui-component/grid/Grid.js create mode 100644 packages/ui/src/views/tools/ToolDialog.js create mode 100644 packages/ui/src/views/tools/index.js diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 9efe602f..4c740874 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,9 +1,10 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' -import { Tool } from 'langchain/tools' import { CustomChainHandler, getBaseClasses } from '../../../src/utils' import { BaseLanguageModel } from 'langchain/base_language' import { flatten } from 'lodash' +import { BaseChatMemory, ChatMessageHistory } from 'langchain/memory' +import { AIChatMessage, HumanChatMessage } from 'langchain/schema' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -30,6 +31,11 @@ class OpenAIFunctionAgent_Agents implements INode { type: 'Tool', list: true }, + { + label: 'Memory', + name: 'memory', + type: 'BaseChatMemory' + }, { label: 'OpenAI Chat Model', name: 'model', @@ -42,18 +48,38 @@ class OpenAIFunctionAgent_Agents implements INode { async init(nodeData: INodeData): Promise { const model = nodeData.inputs?.model as BaseLanguageModel - let tools = nodeData.inputs?.tools as Tool[] + const memory = nodeData.inputs?.memory as BaseChatMemory + + let tools = nodeData.inputs?.tools tools = flatten(tools) const executor = await initializeAgentExecutorWithOptions(tools, model, { agentType: 'openai-functions', verbose: process.env.DEBUG === 'true' ? true : false }) + if (memory) executor.memory = memory + return executor } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const executor = nodeData.instance as AgentExecutor + const memory = nodeData.inputs?.memory as BaseChatMemory + + if (options && options.chatHistory) { + const chatHistory = [] + const histories: IMessage[] = options.chatHistory + + for (const message of histories) { + if (message.type === 'apiMessage') { + chatHistory.push(new AIChatMessage(message.message)) + } else if (message.type === 'userMessage') { + chatHistory.push(new HumanChatMessage(message.message)) + } + } + memory.chatHistory = new ChatMessageHistory(chatHistory) + executor.memory = memory + } if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) diff --git a/packages/components/nodes/tools/ChainTool/chaintool.svg b/packages/components/nodes/tools/ChainTool/chaintool.svg index c5bd0fbc..ab76749b 100644 --- a/packages/components/nodes/tools/ChainTool/chaintool.svg +++ b/packages/components/nodes/tools/ChainTool/chaintool.svg @@ -1,4 +1,8 @@ - + - + + + + + \ No newline at end of file diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts new file mode 100644 index 00000000..768e9092 --- /dev/null +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -0,0 +1,108 @@ +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { DynamicStructuredTool } from './core' +import { z } from 'zod' +import { DataSource } from 'typeorm' + +class CustomTool_Tools implements INode { + label: string + name: string + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Custom Tool' + this.name = 'customTool' + this.type = 'CustomTool' + this.icon = 'customtool.svg' + this.category = 'Tools' + this.description = `Use custom tool you've created in Flowise within chatflow` + this.inputs = [ + { + label: 'Select Tool', + name: 'selectedTool', + type: 'asyncOptions', + loadMethod: 'listTools' + } + ] + this.baseClasses = [this.type, 'Tool', ...getBaseClasses(DynamicStructuredTool)] + } + + //@ts-ignore + loadMethods = { + async listTools(nodeData: INodeData, options: ICommonObject): Promise { + const returnData: INodeOptionsValue[] = [] + + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + if (appDataSource === undefined || !appDataSource) { + return returnData + } + + const tools = await appDataSource.getRepository(databaseEntities['Tool']).find() + + for (let i = 0; i < tools.length; i += 1) { + const data = { + label: tools[i].name, + name: tools[i].id, + description: tools[i].description + } as INodeOptionsValue + returnData.push(data) + } + return returnData + } + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const selectedToolId = nodeData.inputs?.selectedTool as string + + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + try { + const tool = await appDataSource.getRepository(databaseEntities['Tool']).findOneBy({ + id: selectedToolId + }) + + if (!tool) throw new Error(`Tool ${selectedToolId} not found`) + const obj = { + name: tool.name, + description: tool.description, + schema: z.object(convertSchemaToZod(tool.schema)), + code: tool.func + } + return new DynamicStructuredTool(obj) + } catch (e) { + throw new Error(e) + } + } +} + +const convertSchemaToZod = (schema: string) => { + try { + const parsedSchema = JSON.parse(schema) + const zodObj: any = {} + for (const sch of parsedSchema) { + if (sch.type === 'string') { + if (sch.required) z.string({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.string().describe(sch.description) + } else if (sch.type === 'number') { + if (sch.required) z.number({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.number().describe(sch.description) + } else if (sch.type === 'boolean') { + if (sch.required) z.boolean({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.boolean().describe(sch.description) + } + } + return zodObj + } catch (e) { + throw new Error(e) + } +} + +module.exports = { nodeClass: CustomTool_Tools } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts new file mode 100644 index 00000000..0d3d7bcd --- /dev/null +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -0,0 +1,78 @@ +import { z } from 'zod' +import { CallbackManagerForToolRun } from 'langchain/callbacks' +import { StructuredTool, ToolParams } from 'langchain/tools' +import { NodeVM } from 'vm2' +import { availableDependencies } from '../../../src/utils' + +export interface BaseDynamicToolInput extends ToolParams { + name: string + description: string + code: string + returnDirect?: boolean +} + +export interface DynamicStructuredToolInput< + // eslint-disable-next-line + T extends z.ZodObject = z.ZodObject +> extends BaseDynamicToolInput { + func?: (input: z.infer, runManager?: CallbackManagerForToolRun) => Promise + schema: T +} + +export class DynamicStructuredTool< + // eslint-disable-next-line + T extends z.ZodObject = z.ZodObject +> extends StructuredTool { + name: string + + description: string + + code: string + + func: DynamicStructuredToolInput['func'] + + schema: T + + constructor(fields: DynamicStructuredToolInput) { + super(fields) + this.name = fields.name + this.description = fields.description + this.code = fields.code + this.func = fields.func + this.returnDirect = fields.returnDirect ?? this.returnDirect + this.schema = fields.schema + } + + protected async _call(arg: z.output): Promise { + let sandbox: any = {} + if (typeof arg === 'object' && Object.keys(arg).length) { + for (const item in arg) { + sandbox[`$${item}`] = arg[item] + } + } + + const options = { + console: 'inherit', + sandbox, + require: { + external: false as boolean | { modules: string[] }, + builtin: ['*'] + } + } as any + + const external = JSON.stringify(availableDependencies) + if (external) { + const deps = JSON.parse(external) + if (deps && deps.length) { + options.require.external = { + modules: deps + } + } + } + + const vm = new NodeVM(options) + const response = await vm.run(`module.exports = async function() {${this.code}}()`, __dirname) + + return response + } +} diff --git a/packages/components/nodes/tools/CustomTool/customtool.svg b/packages/components/nodes/tools/CustomTool/customtool.svg new file mode 100644 index 00000000..c5bd0fbc --- /dev/null +++ b/packages/components/nodes/tools/CustomTool/customtool.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index 738c7752..ec97d4d7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -33,7 +33,7 @@ "form-data": "^4.0.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", - "langchain": "^0.0.94", + "langchain": "^0.0.96", "linkifyjs": "^4.1.1", "mammoth": "^1.5.1", "moment": "^2.29.3", @@ -43,6 +43,7 @@ "playwright": "^1.35.0", "puppeteer": "^20.7.1", "srt-parser-2": "^1.2.3", + "vm2": "^3.9.19", "weaviate-ts-client": "^1.1.0", "ws": "^8.9.0" }, diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index bd94cca8..f8a6fd58 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -2,7 +2,18 @@ * Types */ -export type NodeParamsType = 'options' | 'string' | 'number' | 'boolean' | 'password' | 'json' | 'code' | 'date' | 'file' | 'folder' +export type NodeParamsType = + | 'asyncOptions' + | 'options' + | 'string' + | 'number' + | 'boolean' + | 'password' + | 'json' + | 'code' + | 'date' + | 'file' + | 'folder' export type CommonType = string | number | boolean | undefined | null @@ -16,6 +27,10 @@ export interface ICommonObject { [key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[] } +export type IDatabaseEntity = { + [key: string]: any +} + export interface IAttachment { content: string contentType: string @@ -50,6 +65,7 @@ export interface INodeParams { placeholder?: string fileType?: string additionalParams?: boolean + loadMethod?: string } export interface INodeExecutionData { @@ -74,6 +90,9 @@ export interface INodeProperties { export interface INode extends INodeProperties { inputs?: INodeParams[] output?: INodeOutputsValue[] + loadMethods?: { + [key: string]: (nodeData: INodeData, options?: ICommonObject) => Promise + } init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise } @@ -83,6 +102,7 @@ export interface INodeData extends INodeProperties { inputs?: ICommonObject outputs?: ICommonObject instance?: any + loadMethod?: string // method to load async options } export interface IMessage { diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index de026a35..ad8d28dc 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -18,6 +18,7 @@ export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is no */ export const getBaseClasses = (targetClass: any) => { const baseClasses: string[] = [] + const skipClassNames = ['BaseLangChain', 'Serializable'] if (targetClass instanceof Function) { let baseClass = targetClass @@ -26,7 +27,7 @@ export const getBaseClasses = (targetClass: any) => { const newBaseClass = Object.getPrototypeOf(baseClass) if (newBaseClass && newBaseClass !== Object && newBaseClass.name) { baseClass = newBaseClass - baseClasses.push(baseClass.name) + if (!skipClassNames.includes(baseClass.name)) baseClasses.push(baseClass.name) } else { break } @@ -284,3 +285,31 @@ const handleEscapeDoubleQuote = (value: string): string => { } return newValue === '' ? value : newValue } + +export const availableDependencies = [ + '@dqbd/tiktoken', + '@getzep/zep-js', + '@huggingface/inference', + '@pinecone-database/pinecone', + '@supabase/supabase-js', + 'axios', + 'cheerio', + 'chromadb', + 'cohere-ai', + 'd3-dsv', + 'form-data', + 'graphql', + 'html-to-text', + 'langchain', + 'linkifyjs', + 'mammoth', + 'moment', + 'node-fetch', + 'pdf-parse', + 'pdfjs-dist', + 'playwright', + 'puppeteer', + 'srt-parser-2', + 'typeorm', + 'weaviate-ts-client' +] diff --git a/packages/server/src/ChildProcess.ts b/packages/server/src/ChildProcess.ts index 08847a52..95f7368a 100644 --- a/packages/server/src/ChildProcess.ts +++ b/packages/server/src/ChildProcess.ts @@ -1,5 +1,10 @@ +import path from 'path' import { IChildProcessMessage, IReactFlowNode, IReactFlowObject, IRunChatflowMessageValue, INodeData } from './Interface' -import { buildLangchain, constructGraphs, getEndingNode, getStartingNodes, resolveVariables } from './utils' +import { buildLangchain, constructGraphs, getEndingNode, getStartingNodes, getUserHome, resolveVariables } from './utils' +import { DataSource } from 'typeorm' +import { ChatFlow } from './entity/ChatFlow' +import { ChatMessage } from './entity/ChatMessage' +import { Tool } from './entity/Tool' export class ChildProcess { /** @@ -22,6 +27,8 @@ export class ChildProcess { await sendToParentProcess('start', '_') + const childAppDataSource = await initDB() + // Create a Queue and add our initial node in it const { endingNodeData, chatflow, chatId, incomingInput, componentNodes } = messageValue @@ -84,6 +91,7 @@ export class ChildProcess { componentNodes, incomingInput.question, chatId, + childAppDataSource, incomingInput?.overrideConfig ) @@ -115,6 +123,22 @@ export class ChildProcess { } } +/** + * Initalize DB in child process + * @returns {DataSource} + */ +async function initDB() { + const homePath = path.join(getUserHome(), '.flowise') + const childAppDataSource = new DataSource({ + type: 'sqlite', + database: path.resolve(homePath, 'database.sqlite'), + synchronize: true, + entities: [ChatFlow, ChatMessage, Tool], + migrations: [] + }) + return await childAppDataSource.initialize() +} + /** * Send data back to parent process * @param {string} key Key of message diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 76c8e144..2ec8104a 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -3,6 +3,7 @@ import path from 'path' import { DataSource } from 'typeorm' import { ChatFlow } from './entity/ChatFlow' import { ChatMessage } from './entity/ChatMessage' +import { Tool } from './entity/Tool' import { getUserHome } from './utils' let appDataSource: DataSource @@ -14,7 +15,7 @@ export const init = async (): Promise => { type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'), synchronize: true, - entities: [ChatFlow, ChatMessage], + entities: [ChatFlow, ChatMessage, Tool], migrations: [] }) } diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 2c1fe406..1eafcae6 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -24,6 +24,17 @@ export interface IChatMessage { sourceDocuments: string } +export interface ITool { + id: string + name: string + description: string + color: string + schema: string + func: string + updatedDate: Date + createdDate: Date +} + export interface IComponentNodes { [key: string]: INode } diff --git a/packages/server/src/entity/Tool.ts b/packages/server/src/entity/Tool.ts new file mode 100644 index 00000000..d547374c --- /dev/null +++ b/packages/server/src/entity/Tool.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm' +import { ITool } from '../Interface' + +@Entity() +export class Tool implements ITool { + @PrimaryGeneratedColumn('uuid') + id: string + + @Column() + name: string + + @Column() + description: string + + @Column() + color: string + + @Column({ nullable: true }) + schema: string + + @Column({ nullable: true }) + func: string + + @CreateDateColumn() + createdDate: Date + + @UpdateDateColumn() + updatedDate: Date +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 40bd75cd..65bfef23 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -35,7 +35,8 @@ import { isSameOverrideConfig, replaceAllAPIKeys, isFlowValidForStream, - isVectorStoreFaiss + isVectorStoreFaiss, + databaseEntities } from './utils' import { cloneDeep } from 'lodash' import { getDataSource } from './DataSource' @@ -43,8 +44,9 @@ import { NodesPool } from './NodesPool' import { ChatFlow } from './entity/ChatFlow' import { ChatMessage } from './entity/ChatMessage' import { ChatflowPool } from './ChatflowPool' -import { ICommonObject } from 'flowise-components' +import { ICommonObject, INodeOptionsValue } from 'flowise-components' import { fork } from 'child_process' +import { Tool } from './entity/Tool' export class App { app: express.Application @@ -142,6 +144,29 @@ export class App { } }) + // load async options + this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { + const nodeData: INodeData = req.body + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + try { + const nodeInstance = this.nodesPool.componentNodes[req.params.name] + const methodName = nodeData.loadMethod || '' + + const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { + appDataSource: this.AppDataSource, + databaseEntities: databaseEntities + }) + + return res.json(returnOptions) + } catch (error) { + return res.json([]) + } + } else { + res.status(404).send(`Node ${req.params.name} not found`) + return + } + }) + // ---------------------------------------- // Chatflows // ---------------------------------------- @@ -257,6 +282,63 @@ export class App { return res.json(results) }) + // ---------------------------------------- + // Tools + // ---------------------------------------- + + // Get all tools + this.app.get('/api/v1/tools', async (req: Request, res: Response) => { + const tools = await this.AppDataSource.getRepository(Tool).find() + return res.json(tools) + }) + + // Get specific tool + this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { + const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ + id: req.params.id + }) + return res.json(tool) + }) + + // Add tool + this.app.post('/api/v1/tools', async (req: Request, res: Response) => { + const body = req.body + const newTool = new Tool() + Object.assign(newTool, body) + + const tool = this.AppDataSource.getRepository(Tool).create(newTool) + const results = await this.AppDataSource.getRepository(Tool).save(tool) + + return res.json(results) + }) + + // Update tool + this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { + const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ + id: req.params.id + }) + + if (!tool) { + res.status(404).send(`Tool ${req.params.id} not found`) + return + } + + const body = req.body + const updateTool = new Tool() + Object.assign(updateTool, body) + + this.AppDataSource.getRepository(Tool).merge(tool, updateTool) + const result = await this.AppDataSource.getRepository(Tool).save(tool) + + return res.json(result) + }) + + // Delete tool + this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { + const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) + return res.json(results) + }) + // ---------------------------------------- // Configuration // ---------------------------------------- @@ -623,6 +705,7 @@ export class App { this.nodesPool.componentNodes, incomingInput.question, chatId, + this.AppDataSource, incomingInput?.overrideConfig ) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 8e19bf5b..e3005c7b 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -15,10 +15,15 @@ import { IOverrideConfig } from '../Interface' import { cloneDeep, get, omit, merge } from 'lodash' -import { ICommonObject, getInputVariables } from 'flowise-components' +import { ICommonObject, getInputVariables, IDatabaseEntity } from 'flowise-components' import { scryptSync, randomBytes, timingSafeEqual } from 'crypto' +import { ChatFlow } from '../entity/ChatFlow' +import { ChatMessage } from '../entity/ChatMessage' +import { Tool } from '../entity/Tool' +import { DataSource } from 'typeorm' const QUESTION_VAR_PREFIX = 'question' +export const databaseEntities: IDatabaseEntity = { ChatFlow: ChatFlow, ChatMessage: ChatMessage, Tool: Tool } /** * Returns the home folder path of the user if @@ -183,6 +188,7 @@ export const buildLangchain = async ( componentNodes: IComponentNodes, question: string, chatId: string, + appDataSource: DataSource, overrideConfig?: ICommonObject ) => { const flowNodes = cloneDeep(reactFlowNodes) @@ -215,7 +221,11 @@ export const buildLangchain = async ( if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question) - flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, { chatId }) + flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, { + chatId, + appDataSource, + databaseEntities + }) } catch (e: any) { console.error(e) throw new Error(e) diff --git a/packages/ui/package.json b/packages/ui/package.json index af9ac5e0..1d7bc490 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,6 +13,7 @@ "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.0.3", "@mui/material": "^5.11.12", + "@mui/x-data-grid": "^6.8.0", "@tabler/icons": "^1.39.1", "clsx": "^1.1.1", "formik": "^2.2.6", diff --git a/packages/ui/src/api/tools.js b/packages/ui/src/api/tools.js new file mode 100644 index 00000000..77992a2a --- /dev/null +++ b/packages/ui/src/api/tools.js @@ -0,0 +1,19 @@ +import client from './client' + +const getAllTools = () => client.get('/tools') + +const getSpecificTool = (id) => client.get(`/tools/${id}`) + +const createNewTool = (body) => client.post(`/tools`, body) + +const updateTool = (id, body) => client.put(`/tools/${id}`, body) + +const deleteTool = (id) => client.delete(`/tools/${id}`) + +export default { + getAllTools, + getSpecificTool, + createNewTool, + updateTool, + deleteTool +} diff --git a/packages/ui/src/assets/images/tools_empty.svg b/packages/ui/src/assets/images/tools_empty.svg new file mode 100644 index 00000000..9a2a2a77 --- /dev/null +++ b/packages/ui/src/assets/images/tools_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/menu-items/dashboard.js b/packages/ui/src/menu-items/dashboard.js index f1cd5062..948b4e4a 100644 --- a/packages/ui/src/menu-items/dashboard.js +++ b/packages/ui/src/menu-items/dashboard.js @@ -1,8 +1,8 @@ // assets -import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons' +import { IconHierarchy, IconBuildingStore, IconKey, IconTool } from '@tabler/icons' // constant -const icons = { IconHierarchy, IconBuildingStore, IconKey } +const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool } // ==============================|| DASHBOARD MENU ITEMS ||============================== // @@ -27,6 +27,14 @@ const dashboard = { icon: icons.IconBuildingStore, breadcrumbs: true }, + { + id: 'tools', + title: 'Tools', + type: 'item', + url: '/tools', + icon: icons.IconTool, + breadcrumbs: true + }, { id: 'apikey', title: 'API Keys', diff --git a/packages/ui/src/routes/MainRoutes.js b/packages/ui/src/routes/MainRoutes.js index 5353e41a..28e60287 100644 --- a/packages/ui/src/routes/MainRoutes.js +++ b/packages/ui/src/routes/MainRoutes.js @@ -13,6 +13,9 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces'))) // apikey routing const APIKey = Loadable(lazy(() => import('views/apikey'))) +// apikey routing +const Tools = Loadable(lazy(() => import('views/tools'))) + // ==============================|| MAIN ROUTING ||============================== // const MainRoutes = { @@ -34,6 +37,10 @@ const MainRoutes = { { path: '/apikey', element: + }, + { + path: '/tools', + element: } ] } diff --git a/packages/ui/src/ui-component/cards/ItemCard.js b/packages/ui/src/ui-component/cards/ItemCard.js index 506947ce..345a88d5 100644 --- a/packages/ui/src/ui-component/cards/ItemCard.js +++ b/packages/ui/src/ui-component/cards/ItemCard.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types' // material-ui -import { styled, useTheme } from '@mui/material/styles' -import { Box, Grid, Chip, Typography } from '@mui/material' +import { styled } from '@mui/material/styles' +import { Box, Grid, Typography } from '@mui/material' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -27,20 +27,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({ // ===========================|| CONTRACT CARD ||=========================== // -const ItemCard = ({ isLoading, data, images, onClick }) => { - const theme = useTheme() - - const chipSX = { - height: 24, - padding: '0 6px' - } - - const activeChatflowSX = { - ...chipSX, - color: 'white', - backgroundColor: theme.palette.success.dark - } - +const ItemCard = ({ isLoading, data, images, color, onClick }) => { return ( <> {isLoading ? ( @@ -49,7 +36,24 @@ const ItemCard = ({ isLoading, data, images, onClick }) => { -
+
+ {color && ( +
+ )} @@ -61,13 +65,6 @@ const ItemCard = ({ isLoading, data, images, onClick }) => { {data.description} )} - - {data.deployed && ( - - - - )} - {images && (
{ + const loadMethod = nodeData.inputParams.find((param) => param.name === name)?.loadMethod + const username = localStorage.getItem('username') + const password = localStorage.getItem('password') + + let lists = await axios + .post( + `${baseURL}/api/v1/node-load-method/${nodeData.name}`, + { ...nodeData, loadMethod }, + { auth: username && password ? { username, password } : undefined } + ) + .then(async function (response) { + return response.data + }) + .catch(function (error) { + console.error(error) + }) + return lists +} + +export const AsyncDropdown = ({ + name, + nodeData, + value, + onSelect, + isCreateNewOption, + onCreateNew, + disabled = false, + disableClearable = false +}) => { + const customization = useSelector((state) => state.customization) + + const [open, setOpen] = useState(false) + const [options, setOptions] = useState([]) + const [loading, setLoading] = useState(false) + const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value) + const getDefaultOptionValue = () => '' + const addNewOption = [{ label: '- Create New -', name: '-create-' }] + let [internalValue, setInternalValue] = useState(value ?? 'choose an option') + + useEffect(() => { + setLoading(true) + ;(async () => { + const fetchData = async () => { + let response = await fetchList({ name, nodeData }) + if (isCreateNewOption) setOptions([...response, ...addNewOption]) + else setOptions([...response]) + setLoading(false) + } + fetchData() + })() + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + <> + { + setOpen(true) + }} + onClose={() => { + setOpen(false) + }} + options={options} + value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()} + onChange={(e, selection) => { + const value = selection ? selection.name : '' + if (isCreateNewOption && value === '-create-') { + onCreateNew() + } else { + setInternalValue(value) + onSelect(value) + } + }} + PopperComponent={StyledPopper} + loading={loading} + renderInput={(params) => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + renderOption={(props, option) => ( + +
+ {option.label} + {option.description && ( + {option.description} + )} +
+
+ )} + /> + + ) +} + +AsyncDropdown.propTypes = { + name: PropTypes.string, + nodeData: PropTypes.object, + value: PropTypes.string, + onSelect: PropTypes.func, + onCreateNew: PropTypes.func, + disabled: PropTypes.bool, + disableClearable: PropTypes.bool, + isCreateNewOption: PropTypes.bool +} diff --git a/packages/ui/src/ui-component/editor/DarkCodeEditor.js b/packages/ui/src/ui-component/editor/DarkCodeEditor.js index 3925f4a6..bf0719dd 100644 --- a/packages/ui/src/ui-component/editor/DarkCodeEditor.js +++ b/packages/ui/src/ui-component/editor/DarkCodeEditor.js @@ -21,6 +21,7 @@ export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, sty onValueChange={onValueChange} onMouseUp={onMouseUp} onBlur={onBlur} + tabSize={4} style={{ ...style, background: theme.palette.codeEditor.main diff --git a/packages/ui/src/ui-component/editor/LightCodeEditor.js b/packages/ui/src/ui-component/editor/LightCodeEditor.js index 86f7057d..14dcbf29 100644 --- a/packages/ui/src/ui-component/editor/LightCodeEditor.js +++ b/packages/ui/src/ui-component/editor/LightCodeEditor.js @@ -21,6 +21,7 @@ export const LightCodeEditor = ({ value, placeholder, disabled = false, type, st onValueChange={onValueChange} onMouseUp={onMouseUp} onBlur={onBlur} + tabSize={4} style={{ ...style, background: theme.palette.card.main diff --git a/packages/ui/src/ui-component/grid/Grid.js b/packages/ui/src/ui-component/grid/Grid.js new file mode 100644 index 00000000..2049f56c --- /dev/null +++ b/packages/ui/src/ui-component/grid/Grid.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types' +import { DataGrid } from '@mui/x-data-grid' +import { IconPlus } from '@tabler/icons' +import { Button } from '@mui/material' + +export const Grid = ({ columns, rows, style, onRowUpdate, addNewRow }) => { + const handleProcessRowUpdate = (newRow) => { + onRowUpdate(newRow) + return newRow + } + + return ( + <> + + {rows && columns && ( +
+ console.error(error)} + rows={rows} + columns={columns} + /> +
+ )} + + ) +} + +Grid.propTypes = { + rows: PropTypes.array, + columns: PropTypes.array, + style: PropTypes.any, + addNewRow: PropTypes.func, + onRowUpdate: PropTypes.func +} diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index fac83225..03f891ec 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -39,7 +39,7 @@ export const initNode = (nodeData, newNodeId) => { const incoming = nodeData.inputs ? nodeData.inputs.length : 0 const outgoing = 1 - const whitelistTypes = ['options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder'] + const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder'] for (let i = 0; i < incoming; i += 1) { const newInput = { @@ -334,3 +334,22 @@ export const throttle = (func, limit) => { } } } + +export const generateRandomGradient = () => { + function randomColor() { + var color = 'rgb(' + for (var i = 0; i < 3; i++) { + var random = Math.floor(Math.random() * 256) + color += random + if (i < 2) { + color += ',' + } + } + color += ')' + return color + } + + var gradient = 'linear-gradient(' + randomColor() + ', ' + randomColor() + ')' + + return gradient +} diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index d58f7a66..31a8a37d 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -7,10 +7,11 @@ import { useSelector } from 'react-redux' import { useTheme, styled } from '@mui/material/styles' import { Box, Typography, Tooltip, IconButton } from '@mui/material' import { tooltipClasses } from '@mui/material/Tooltip' -import { IconArrowsMaximize } from '@tabler/icons' +import { IconArrowsMaximize, IconEdit } from '@tabler/icons' // project import import { Dropdown } from 'ui-component/dropdown/Dropdown' +import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown' import { Input } from 'ui-component/input/Input' import { File } from 'ui-component/file/File' import { SwitchInput } from 'ui-component/switch/Switch' @@ -18,6 +19,9 @@ import { flowContext } from 'store/context/ReactFlowContext' import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper' import { JsonEditorInput } from 'ui-component/json/JsonEditor' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import ToolDialog from 'views/tools/ToolDialog' + +const EDITABLE_TOOLS = ['selectedTool'] const CustomWidthTooltip = styled(({ className, ...props }) => )({ [`& .${tooltipClasses.tooltip}`]: { @@ -36,6 +40,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA const [position, setPosition] = useState(0) const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) + const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('') + const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({}) + const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) const onExpandDialogClicked = (value, inputParam) => { const dialogProp = { @@ -61,6 +68,42 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA data.inputs[inputParamName] = newValue } + const editAsyncOption = (inputParamName, inputValue) => { + if (inputParamName === 'selectedTool') { + setAsyncOptionEditDialogProps({ + title: 'Edit Tool', + type: 'EDIT', + cancelButtonName: 'Cancel', + confirmButtonName: 'Save', + toolId: inputValue + }) + } + setAsyncOptionEditDialog(inputParamName) + } + + const addAsyncOption = (inputParamName) => { + if (inputParamName === 'selectedTool') { + setAsyncOptionEditDialogProps({ + title: 'Add New Tool', + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Add' + }) + } + setAsyncOptionEditDialog(inputParamName) + } + + const onConfirmAsyncOption = (selectedOptionId = '') => { + if (!selectedOptionId) { + data.inputs[showAsyncOptionDialog] = '' + } else { + data.inputs[showAsyncOptionDialog] = selectedOptionId + setReloadTimestamp(Date.now().toString()) + } + setAsyncOptionEditDialogProps({}) + setAsyncOptionEditDialog('') + } + useEffect(() => { if (ref.current && ref.current.offsetTop && ref.current.clientHeight) { setPosition(ref.current.offsetTop + ref.current.clientHeight / 2) @@ -186,12 +229,44 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA name={inputParam.name} options={inputParam.options} onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} - value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'} + value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} /> )} + {inputParam.type === 'asyncOptions' && ( + <> + {data.inputParams.length === 1 &&
} +
+ (data.inputs[inputParam.name] = newValue)} + onCreateNew={() => addAsyncOption(inputParam.name)} + /> + {EDITABLE_TOOLS.includes(inputParam.name) && data.inputs[inputParam.name] && ( + editAsyncOption(inputParam.name, data.inputs[inputParam.name])} + > + + + )} +
+ + )} )} + setAsyncOptionEditDialog('')} + onConfirm={onConfirmAsyncOption} + >
) } diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js new file mode 100644 index 00000000..bd5af355 --- /dev/null +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -0,0 +1,448 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { useState, useEffect, useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' +import { cloneDeep } from 'lodash' + +import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material' +import { StyledButton } from 'ui-component/button/StyledButton' +import { Grid } from 'ui-component/grid/Grid' +import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import { GridActionsCellItem } from '@mui/x-data-grid' +import DeleteIcon from '@mui/icons-material/Delete' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' +import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' +import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' +import { useTheme } from '@mui/material/styles' + +// Icons +import { IconX } from '@tabler/icons' + +// API +import toolsApi from 'api/tools' + +// Hooks +import useConfirm from 'hooks/useConfirm' +import useApi from 'hooks/useApi' + +// utils +import useNotifier from 'utils/useNotifier' +import { generateRandomGradient } from 'utils/genericHelper' + +const exampleAPIFunc = `/* +* You can use any libraries imported in Flowise +* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid +* Must return a string value at the end of function +*/ + +const fetch = require('node-fetch'); +const url = 'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_weather=true'; +const options = { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } +}; +try { + const response = await fetch(url, options); + const text = await response.text(); + return text; +} catch (error) { + console.error(error); + return ''; +}` + +const ToolDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + const theme = useTheme() + + const customization = useSelector((state) => state.customization) + const dispatch = useDispatch() + + // ==============================|| Snackbar ||============================== // + + useNotifier() + const { confirm } = useConfirm() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const getSpecificToolApi = useApi(toolsApi.getSpecificTool) + + const [toolId, setToolId] = useState('') + const [toolName, setToolName] = useState('') + const [toolDesc, setToolDesc] = useState('') + const [toolSchema, setToolSchema] = useState([]) + const [toolFunc, setToolFunc] = useState('') + + const deleteItem = useCallback( + (id) => () => { + setTimeout(() => { + setToolSchema((prevRows) => prevRows.filter((row) => row.id !== id)) + }) + }, + [] + ) + + const addNewRow = () => { + setTimeout(() => { + setToolSchema((prevRows) => { + let allRows = [...cloneDeep(prevRows)] + const lastRowId = allRows.length ? allRows[allRows.length - 1].id + 1 : 1 + allRows.push({ + id: lastRowId, + property: '', + description: '', + type: '', + required: false + }) + return allRows + }) + }) + } + + const onRowUpdate = (newRow) => { + setTimeout(() => { + setToolSchema((prevRows) => { + let allRows = [...cloneDeep(prevRows)] + const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id) + if (indexToUpdate >= 0) { + allRows[indexToUpdate] = { ...newRow } + } + return allRows + }) + }) + } + + const columns = useMemo( + () => [ + { field: 'property', headerName: 'Property', editable: true, flex: 1 }, + { + field: 'type', + headerName: 'Type', + type: 'singleSelect', + valueOptions: ['string', 'number', 'boolean', 'date'], + editable: true, + width: 120 + }, + { field: 'description', headerName: 'Description', editable: true, flex: 1 }, + { field: 'required', headerName: 'Required', type: 'boolean', editable: true, width: 80 }, + { + field: 'actions', + type: 'actions', + width: 80, + getActions: (params) => [ + } label='Delete' onClick={deleteItem(params.id)} /> + ] + } + ], + [deleteItem] + ) + + const formatSchema = (schema) => { + try { + const parsedSchema = JSON.parse(schema) + return parsedSchema.map((sch, index) => { + return { + ...sch, + id: index + } + }) + } catch (e) { + return [] + } + } + + useEffect(() => { + if (getSpecificToolApi.data) { + setToolId(getSpecificToolApi.data.id) + setToolName(getSpecificToolApi.data.name) + setToolDesc(getSpecificToolApi.data.description) + setToolSchema(formatSchema(getSpecificToolApi.data.schema)) + if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func) + else setToolFunc('') + } + }, [getSpecificToolApi.data]) + + useEffect(() => { + if (dialogProps.type === 'EDIT' && dialogProps.data) { + setToolId(dialogProps.data.id) + setToolName(dialogProps.data.name) + setToolDesc(dialogProps.data.description) + setToolSchema(formatSchema(dialogProps.data.schema)) + if (dialogProps.data.func) setToolFunc(dialogProps.data.func) + else setToolFunc('') + } else if (dialogProps.type === 'EDIT' && dialogProps.toolId) { + getSpecificToolApi.request(dialogProps.toolId) + } else if (dialogProps.type === 'ADD') { + setToolId('') + setToolName('') + setToolDesc('') + setToolSchema([]) + setToolFunc('') + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + const addNewTool = async () => { + try { + const obj = { + name: toolName, + description: toolDesc, + color: generateRandomGradient(), + schema: JSON.stringify(toolSchema), + func: toolFunc + } + const createResp = await toolsApi.createNewTool(obj) + if (createResp.data) { + enqueueSnackbar({ + message: 'New Tool added', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(createResp.data.id) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to add new Tool: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const saveTool = async () => { + try { + const saveResp = await toolsApi.updateTool(toolId, { + name: toolName, + description: toolDesc, + schema: JSON.stringify(toolSchema), + func: toolFunc + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Tool saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(saveResp.data.id) + } + } catch (error) { + console.error(error) + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Tool: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const deleteTool = async () => { + const confirmPayload = { + title: `Delete Tool`, + description: `Delete tool ${toolName}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + const delResp = await toolsApi.deleteTool(toolId) + if (delResp.data) { + enqueueSnackbar({ + message: 'Tool deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to delete Tool: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + } + + const component = show ? ( + + + {dialogProps.title} + + + + + + Tool Name +  * + + + setToolName(e.target.value)} + /> + + + + + Tool description +  * + + + setToolDesc(e.target.value)} + /> + + + + + Output Schema + + + + + + + + + Javascript Function + + + + + {customization.isDarkMode ? ( + setToolFunc(code)} + style={{ + fontSize: '0.875rem', + minHeight: 'calc(100vh - 220px)', + width: '100%', + borderRadius: 5 + }} + /> + ) : ( + setToolFunc(code)} + style={{ + fontSize: '0.875rem', + minHeight: 'calc(100vh - 220px)', + width: '100%', + border: `1px solid ${theme.palette.grey[300]}`, + borderRadius: 5 + }} + /> + )} + + + + {dialogProps.type === 'EDIT' && ( + deleteTool()}> + Delete + + )} + (dialogProps.type === 'ADD' ? addNewTool() : saveTool())} + > + {dialogProps.confirmButtonName} + + + + + ) : null + + return createPortal(component, portalElement) +} + +ToolDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default ToolDialog diff --git a/packages/ui/src/views/tools/index.js b/packages/ui/src/views/tools/index.js new file mode 100644 index 00000000..efe9e69d --- /dev/null +++ b/packages/ui/src/views/tools/index.js @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +// material-ui +import { Grid, Box, Stack } from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// project imports +import MainCard from 'ui-component/cards/MainCard' +import ItemCard from 'ui-component/cards/ItemCard' +import { gridSpacing } from 'store/constant' +import ToolEmptySVG from 'assets/images/tools_empty.svg' +import { StyledButton } from 'ui-component/button/StyledButton' +import ToolDialog from './ToolDialog' + +// API +import toolsApi from 'api/tools' + +// Hooks +import useApi from 'hooks/useApi' + +// icons +import { IconPlus } from '@tabler/icons' + +// ==============================|| CHATFLOWS ||============================== // + +const Tools = () => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const getAllToolsApi = useApi(toolsApi.getAllTools) + + const [showDialog, setShowDialog] = useState(false) + const [dialogProps, setDialogProps] = useState({}) + + const addNew = () => { + const dialogProp = { + title: 'Add New Tool', + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Add' + } + setDialogProps(dialogProp) + setShowDialog(true) + } + + const edit = (selectedTool) => { + const dialogProp = { + title: 'Edit Tool', + type: 'EDIT', + cancelButtonName: 'Cancel', + confirmButtonName: 'Save', + data: selectedTool + } + setDialogProps(dialogProp) + setShowDialog(true) + } + + const onConfirm = () => { + setShowDialog(false) + getAllToolsApi.request() + } + + useEffect(() => { + getAllToolsApi.request() + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + <> + + +

Tools

+ + + + }> + Create New + + + +
+ + {!getAllToolsApi.loading && + getAllToolsApi.data && + getAllToolsApi.data.map((data, index) => ( + + edit(data)} /> + + ))} + + {!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( + + + ToolEmptySVG + +
No Tools Created Yet
+
+ )} +
+ setShowDialog(false)} + onConfirm={onConfirm} + > + + ) +} + +export default Tools From 412539a9db9f865d0ed9142368d65c440cf94427 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 21 Jun 2023 20:56:25 +0100 Subject: [PATCH 2/3] update openai agent --- .../OpenAIFunctionAgent.ts | 14 +- .../server/marketplaces/OpenAI Agent.json | 322 +++++++++++++----- 2 files changed, 244 insertions(+), 92 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 4c740874..1cbcb547 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -42,6 +42,14 @@ class OpenAIFunctionAgent_Agents implements INode { description: 'Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info', type: 'BaseChatModel' + }, + { + label: 'System Message', + name: 'systemMessage', + type: 'string', + rows: 4, + optional: true, + additionalParams: true } ] } @@ -49,13 +57,17 @@ class OpenAIFunctionAgent_Agents implements INode { async init(nodeData: INodeData): Promise { const model = nodeData.inputs?.model as BaseLanguageModel const memory = nodeData.inputs?.memory as BaseChatMemory + const systemMessage = nodeData.inputs?.systemMessage as string let tools = nodeData.inputs?.tools tools = flatten(tools) const executor = await initializeAgentExecutorWithOptions(tools, model, { agentType: 'openai-functions', - verbose: process.env.DEBUG === 'true' ? true : false + verbose: process.env.DEBUG === 'true' ? true : false, + agentArgs: { + prefix: systemMessage ?? `You are a helpful AI assistant.` + } }) if (memory) executor.memory = memory diff --git a/packages/server/marketplaces/OpenAI Agent.json b/packages/server/marketplaces/OpenAI Agent.json index 7e685546..75dc1527 100644 --- a/packages/server/marketplaces/OpenAI Agent.json +++ b/packages/server/marketplaces/OpenAI Agent.json @@ -6,8 +6,8 @@ "height": 524, "id": "chatOpenAI_0", "position": { - "x": 373.8366297840716, - "y": 448.58765780622326 + "x": 648.7470970481406, + "y": 462.3331811694268 }, "type": "customNode", "data": { @@ -34,33 +34,29 @@ "label": "gpt-4", "name": "gpt-4" }, + { + "label": "gpt-4-0314", + "name": "gpt-4-0314" + }, + { + "label": "gpt-4-32k-0314", + "name": "gpt-4-32k-0314" + }, { "label": "gpt-4-0613", "name": "gpt-4-0613" }, - { - "label": "gpt-4-32k", - "name": "gpt-4-32k" - }, - { - "label": "gpt-4-32k-0613", - "name": "gpt-4-32k-0613" - }, { "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0301", + "name": "gpt-3.5-turbo-0301" + }, { "label": "gpt-3.5-turbo-0613", "name": "gpt-3.5-turbo-0613" - }, - { - "label": "gpt-3.5-turbo-16k", - "name": "gpt-3.5-turbo-16k" - }, - { - "label": "gpt-3.5-turbo-16k-0613", - "name": "gpt-3.5-turbo-16k-0613" } ], "default": "gpt-3.5-turbo", @@ -148,64 +144,8 @@ }, "selected": false, "positionAbsolute": { - "x": 373.8366297840716, - "y": 448.58765780622326 - }, - "dragging": false - }, - { - "width": 300, - "height": 280, - "id": "openAIFunctionAgent_0", - "position": { - "x": 1084.5405852317417, - "y": 384.4653768834282 - }, - "type": "customNode", - "data": { - "id": "openAIFunctionAgent_0", - "label": "OpenAI Function Agent", - "name": "openAIFunctionAgent", - "type": "AgentExecutor", - "baseClasses": ["AgentExecutor", "BaseChain", "BaseLangChain", "Serializable"], - "category": "Agents", - "description": "An agent that uses OpenAI's Function Calling functionality to pick the tool and args to call", - "inputParams": [], - "inputAnchors": [ - { - "label": "Allowed Tools", - "name": "tools", - "type": "Tool", - "list": true, - "id": "openAIFunctionAgent_0-input-tools-Tool" - }, - { - "label": "OpenAI Chat Model", - "name": "model", - "description": "Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info", - "type": "BaseChatModel", - "id": "openAIFunctionAgent_0-input-model-BaseChatModel" - } - ], - "inputs": { - "tools": ["{{calculator_0.data.instance}}", "{{serper_0.data.instance}}"], - "model": "{{chatOpenAI_0.data.instance}}" - }, - "outputAnchors": [ - { - "id": "openAIFunctionAgent_0-output-openAIFunctionAgent-AgentExecutor|BaseChain|BaseLangChain|Serializable", - "name": "openAIFunctionAgent", - "label": "AgentExecutor", - "type": "AgentExecutor | BaseChain | BaseLangChain | Serializable" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 1084.5405852317417, - "y": 384.4653768834282 + "x": 648.7470970481406, + "y": 462.3331811694268 }, "dragging": false }, @@ -214,8 +154,8 @@ "height": 278, "id": "serper_0", "position": { - "x": 691.7580226065319, - "y": 34.00444633899792 + "x": 486.27248799490576, + "y": 4.465900738576664 }, "type": "customNode", "data": { @@ -249,8 +189,8 @@ }, "selected": false, "positionAbsolute": { - "x": 691.7580226065319, - "y": 34.00444633899792 + "x": 486.27248799490576, + "y": 4.465900738576664 }, "dragging": false }, @@ -259,8 +199,8 @@ "height": 143, "id": "calculator_0", "position": { - "x": 341.63347110886497, - "y": 261.6753474034481 + "x": 286.4092336819905, + "y": 304.05673891709597 }, "type": "customNode", "data": { @@ -287,20 +227,198 @@ }, "selected": false, "positionAbsolute": { - "x": 341.63347110886497, - "y": 261.6753474034481 + "x": 286.4092336819905, + "y": 304.05673891709597 + }, + "dragging": false + }, + { + "width": 300, + "height": 383, + "id": "openAIFunctionAgent_0", + "position": { + "x": 1341.2259105169032, + "y": 318.35651549722945 + }, + "type": "customNode", + "data": { + "id": "openAIFunctionAgent_0", + "label": "OpenAI Function Agent", + "name": "openAIFunctionAgent", + "type": "AgentExecutor", + "baseClasses": ["AgentExecutor", "BaseChain"], + "category": "Agents", + "description": "An agent that uses OpenAI's Function Calling functionality to pick the tool and args to call", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessage", + "type": "string", + "rows": 4, + "optional": true, + "additionalParams": true, + "id": "openAIFunctionAgent_0-input-systemMessage-string" + } + ], + "inputAnchors": [ + { + "label": "Allowed Tools", + "name": "tools", + "type": "Tool", + "list": true, + "id": "openAIFunctionAgent_0-input-tools-Tool" + }, + { + "label": "Memory", + "name": "memory", + "type": "BaseChatMemory", + "id": "openAIFunctionAgent_0-input-memory-BaseChatMemory" + }, + { + "label": "OpenAI Chat Model", + "name": "model", + "description": "Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info", + "type": "BaseChatModel", + "id": "openAIFunctionAgent_0-input-model-BaseChatModel" + } + ], + "inputs": { + "tools": ["{{serper_0.data.instance}}", "{{calculator_0.data.instance}}", "{{customTool_0.data.instance}}"], + "memory": "{{bufferMemory_0.data.instance}}", + "model": "{{chatOpenAI_0.data.instance}}", + "systemMessage": "" + }, + "outputAnchors": [ + { + "id": "openAIFunctionAgent_0-output-openAIFunctionAgent-AgentExecutor|BaseChain", + "name": "openAIFunctionAgent", + "label": "AgentExecutor", + "type": "AgentExecutor | BaseChain" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1341.2259105169032, + "y": 318.35651549722945 + }, + "dragging": false + }, + { + "width": 300, + "height": 376, + "id": "bufferMemory_0", + "position": { + "x": 285.7750469157585, + "y": 465.1140427303788 + }, + "type": "customNode", + "data": { + "id": "bufferMemory_0", + "label": "Buffer Memory", + "name": "bufferMemory", + "type": "BufferMemory", + "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], + "category": "Memory", + "description": "Remembers previous conversational back and forths directly", + "inputParams": [ + { + "label": "Memory Key", + "name": "memoryKey", + "type": "string", + "default": "chat_history", + "id": "bufferMemory_0-input-memoryKey-string" + }, + { + "label": "Input Key", + "name": "inputKey", + "type": "string", + "default": "input", + "id": "bufferMemory_0-input-inputKey-string" + } + ], + "inputAnchors": [], + "inputs": { + "memoryKey": "chat_history", + "inputKey": "input" + }, + "outputAnchors": [ + { + "id": "bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory", + "name": "bufferMemory", + "label": "BufferMemory", + "type": "BufferMemory | BaseChatMemory | BaseMemory" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 285.7750469157585, + "y": 465.1140427303788 + }, + "dragging": false + }, + { + "width": 300, + "height": 277, + "id": "customTool_0", + "position": { + "x": 883.9529939431576, + "y": -32.32503903826486 + }, + "type": "customNode", + "data": { + "id": "customTool_0", + "label": "Custom Tool", + "name": "customTool", + "type": "CustomTool", + "baseClasses": ["CustomTool", "Tool", "StructuredTool"], + "category": "Tools", + "description": "Use custom tool you've created in Flowise within chatflow", + "inputParams": [ + { + "label": "Select Tool", + "name": "selectedTool", + "type": "asyncOptions", + "loadMethod": "listTools", + "id": "customTool_0-input-selectedTool-asyncOptions" + } + ], + "inputAnchors": [], + "inputs": { + "selectedTool": "" + }, + "outputAnchors": [ + { + "id": "customTool_0-output-customTool-CustomTool|Tool|StructuredTool", + "name": "customTool", + "label": "CustomTool", + "type": "CustomTool | Tool | StructuredTool" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 883.9529939431576, + "y": -32.32503903826486 }, "dragging": false } ], "edges": [ { - "source": "chatOpenAI_0", - "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLangChain|Serializable", + "source": "serper_0", + "sourceHandle": "serper_0-output-serper-Serper|Tool|StructuredTool|BaseLangChain|Serializable", "target": "openAIFunctionAgent_0", - "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", + "targetHandle": "openAIFunctionAgent_0-input-tools-Tool", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLangChain|Serializable-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", + "id": "serper_0-serper_0-output-serper-Serper|Tool|StructuredTool|BaseLangChain|Serializable-openAIFunctionAgent_0-openAIFunctionAgent_0-input-tools-Tool", "data": { "label": "" } @@ -317,12 +435,34 @@ } }, { - "source": "serper_0", - "sourceHandle": "serper_0-output-serper-Serper|Tool|StructuredTool|BaseLangChain|Serializable", + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLangChain|Serializable", + "target": "openAIFunctionAgent_0", + "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLangChain|Serializable-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", + "data": { + "label": "" + } + }, + { + "source": "bufferMemory_0", + "sourceHandle": "bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory", + "target": "openAIFunctionAgent_0", + "targetHandle": "openAIFunctionAgent_0-input-memory-BaseChatMemory", + "type": "buttonedge", + "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-openAIFunctionAgent_0-openAIFunctionAgent_0-input-memory-BaseChatMemory", + "data": { + "label": "" + } + }, + { + "source": "customTool_0", + "sourceHandle": "customTool_0-output-customTool-CustomTool|Tool|StructuredTool", "target": "openAIFunctionAgent_0", "targetHandle": "openAIFunctionAgent_0-input-tools-Tool", "type": "buttonedge", - "id": "serper_0-serper_0-output-serper-Serper|Tool|StructuredTool|BaseLangChain|Serializable-openAIFunctionAgent_0-openAIFunctionAgent_0-input-tools-Tool", + "id": "customTool_0-customTool_0-output-customTool-CustomTool|Tool|StructuredTool-openAIFunctionAgent_0-openAIFunctionAgent_0-input-tools-Tool", "data": { "label": "" } From 4a63b68bbe4d7c63d47d45c8168bc3e95ddef709 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 22 Jun 2023 16:22:12 +0100 Subject: [PATCH 3/3] add DATABSE_PATH --- packages/server/src/ChildProcess.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/ChildProcess.ts b/packages/server/src/ChildProcess.ts index 95f7368a..e8aeaff2 100644 --- a/packages/server/src/ChildProcess.ts +++ b/packages/server/src/ChildProcess.ts @@ -128,7 +128,7 @@ export class ChildProcess { * @returns {DataSource} */ async function initDB() { - const homePath = path.join(getUserHome(), '.flowise') + const homePath = process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise') const childAppDataSource = new DataSource({ type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'),