add custom tool

This commit is contained in:
Henry 2023-06-21 18:31:53 +01:00
parent 8c880199cd
commit 70da39629c
28 changed files with 1346 additions and 43 deletions

View File

@ -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 { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents'
import { Tool } from 'langchain/tools'
import { CustomChainHandler, getBaseClasses } from '../../../src/utils' import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { flatten } from 'lodash' import { flatten } from 'lodash'
import { BaseChatMemory, ChatMessageHistory } from 'langchain/memory'
import { AIChatMessage, HumanChatMessage } from 'langchain/schema'
class OpenAIFunctionAgent_Agents implements INode { class OpenAIFunctionAgent_Agents implements INode {
label: string label: string
@ -30,6 +31,11 @@ class OpenAIFunctionAgent_Agents implements INode {
type: 'Tool', type: 'Tool',
list: true list: true
}, },
{
label: 'Memory',
name: 'memory',
type: 'BaseChatMemory'
},
{ {
label: 'OpenAI Chat Model', label: 'OpenAI Chat Model',
name: 'model', name: 'model',
@ -42,18 +48,38 @@ class OpenAIFunctionAgent_Agents implements INode {
async init(nodeData: INodeData): Promise<any> { async init(nodeData: INodeData): Promise<any> {
const model = nodeData.inputs?.model as BaseLanguageModel 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) tools = flatten(tools)
const executor = await initializeAgentExecutorWithOptions(tools, model, { const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentType: 'openai-functions', agentType: 'openai-functions',
verbose: process.env.DEBUG === 'true' ? true : false verbose: process.env.DEBUG === 'true' ? true : false
}) })
if (memory) executor.memory = memory
return executor return executor
} }
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const executor = nodeData.instance as AgentExecutor 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) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)

View File

@ -1,4 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tool" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-subtask" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"></path> <path d="M6 9l6 0"></path>
<path d="M4 5l4 0"></path>
<path d="M6 5v11a1 1 0 0 0 1 1h5"></path>
<path d="M12 7m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
<path d="M12 15m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 598 B

View File

@ -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<INodeOptionsValue[]> {
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<any> {
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 }

View File

@ -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<any, any, any, any> = z.ZodObject<any, any, any, any>
> extends BaseDynamicToolInput {
func?: (input: z.infer<T>, runManager?: CallbackManagerForToolRun) => Promise<string>
schema: T
}
export class DynamicStructuredTool<
// eslint-disable-next-line
T extends z.ZodObject<any, any, any, any> = z.ZodObject<any, any, any, any>
> extends StructuredTool {
name: string
description: string
code: string
func: DynamicStructuredToolInput['func']
schema: T
constructor(fields: DynamicStructuredToolInput<T>) {
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<T>): Promise<string> {
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
}
}

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tool" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -33,7 +33,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"langchain": "^0.0.94", "langchain": "^0.0.96",
"linkifyjs": "^4.1.1", "linkifyjs": "^4.1.1",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"moment": "^2.29.3", "moment": "^2.29.3",
@ -43,6 +43,7 @@
"playwright": "^1.35.0", "playwright": "^1.35.0",
"puppeteer": "^20.7.1", "puppeteer": "^20.7.1",
"srt-parser-2": "^1.2.3", "srt-parser-2": "^1.2.3",
"vm2": "^3.9.19",
"weaviate-ts-client": "^1.1.0", "weaviate-ts-client": "^1.1.0",
"ws": "^8.9.0" "ws": "^8.9.0"
}, },

View File

@ -2,7 +2,18 @@
* Types * 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 export type CommonType = string | number | boolean | undefined | null
@ -16,6 +27,10 @@ export interface ICommonObject {
[key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[] [key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[]
} }
export type IDatabaseEntity = {
[key: string]: any
}
export interface IAttachment { export interface IAttachment {
content: string content: string
contentType: string contentType: string
@ -50,6 +65,7 @@ export interface INodeParams {
placeholder?: string placeholder?: string
fileType?: string fileType?: string
additionalParams?: boolean additionalParams?: boolean
loadMethod?: string
} }
export interface INodeExecutionData { export interface INodeExecutionData {
@ -74,6 +90,9 @@ export interface INodeProperties {
export interface INode extends INodeProperties { export interface INode extends INodeProperties {
inputs?: INodeParams[] inputs?: INodeParams[]
output?: INodeOutputsValue[] output?: INodeOutputsValue[]
loadMethods?: {
[key: string]: (nodeData: INodeData, options?: ICommonObject) => Promise<INodeOptionsValue[]>
}
init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<any> init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<any>
run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<string | ICommonObject> run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<string | ICommonObject>
} }
@ -83,6 +102,7 @@ export interface INodeData extends INodeProperties {
inputs?: ICommonObject inputs?: ICommonObject
outputs?: ICommonObject outputs?: ICommonObject
instance?: any instance?: any
loadMethod?: string // method to load async options
} }
export interface IMessage { export interface IMessage {

View File

@ -18,6 +18,7 @@ export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is no
*/ */
export const getBaseClasses = (targetClass: any) => { export const getBaseClasses = (targetClass: any) => {
const baseClasses: string[] = [] const baseClasses: string[] = []
const skipClassNames = ['BaseLangChain', 'Serializable']
if (targetClass instanceof Function) { if (targetClass instanceof Function) {
let baseClass = targetClass let baseClass = targetClass
@ -26,7 +27,7 @@ export const getBaseClasses = (targetClass: any) => {
const newBaseClass = Object.getPrototypeOf(baseClass) const newBaseClass = Object.getPrototypeOf(baseClass)
if (newBaseClass && newBaseClass !== Object && newBaseClass.name) { if (newBaseClass && newBaseClass !== Object && newBaseClass.name) {
baseClass = newBaseClass baseClass = newBaseClass
baseClasses.push(baseClass.name) if (!skipClassNames.includes(baseClass.name)) baseClasses.push(baseClass.name)
} else { } else {
break break
} }
@ -284,3 +285,31 @@ const handleEscapeDoubleQuote = (value: string): string => {
} }
return newValue === '' ? value : newValue 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'
]

View File

@ -1,5 +1,10 @@
import path from 'path'
import { IChildProcessMessage, IReactFlowNode, IReactFlowObject, IRunChatflowMessageValue, INodeData } from './Interface' 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 { export class ChildProcess {
/** /**
@ -22,6 +27,8 @@ export class ChildProcess {
await sendToParentProcess('start', '_') await sendToParentProcess('start', '_')
const childAppDataSource = await initDB()
// Create a Queue and add our initial node in it // Create a Queue and add our initial node in it
const { endingNodeData, chatflow, chatId, incomingInput, componentNodes } = messageValue const { endingNodeData, chatflow, chatId, incomingInput, componentNodes } = messageValue
@ -84,6 +91,7 @@ export class ChildProcess {
componentNodes, componentNodes,
incomingInput.question, incomingInput.question,
chatId, chatId,
childAppDataSource,
incomingInput?.overrideConfig 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 * Send data back to parent process
* @param {string} key Key of message * @param {string} key Key of message

View File

@ -3,6 +3,7 @@ import path from 'path'
import { DataSource } from 'typeorm' import { DataSource } from 'typeorm'
import { ChatFlow } from './entity/ChatFlow' import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage' import { ChatMessage } from './entity/ChatMessage'
import { Tool } from './entity/Tool'
import { getUserHome } from './utils' import { getUserHome } from './utils'
let appDataSource: DataSource let appDataSource: DataSource
@ -14,7 +15,7 @@ export const init = async (): Promise<void> => {
type: 'sqlite', type: 'sqlite',
database: path.resolve(homePath, 'database.sqlite'), database: path.resolve(homePath, 'database.sqlite'),
synchronize: true, synchronize: true,
entities: [ChatFlow, ChatMessage], entities: [ChatFlow, ChatMessage, Tool],
migrations: [] migrations: []
}) })
} }

View File

@ -24,6 +24,17 @@ export interface IChatMessage {
sourceDocuments: string sourceDocuments: string
} }
export interface ITool {
id: string
name: string
description: string
color: string
schema: string
func: string
updatedDate: Date
createdDate: Date
}
export interface IComponentNodes { export interface IComponentNodes {
[key: string]: INode [key: string]: INode
} }

View File

@ -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
}

View File

@ -35,7 +35,8 @@ import {
isSameOverrideConfig, isSameOverrideConfig,
replaceAllAPIKeys, replaceAllAPIKeys,
isFlowValidForStream, isFlowValidForStream,
isVectorStoreFaiss isVectorStoreFaiss,
databaseEntities
} from './utils' } from './utils'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource' import { getDataSource } from './DataSource'
@ -43,8 +44,9 @@ import { NodesPool } from './NodesPool'
import { ChatFlow } from './entity/ChatFlow' import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage' import { ChatMessage } from './entity/ChatMessage'
import { ChatflowPool } from './ChatflowPool' import { ChatflowPool } from './ChatflowPool'
import { ICommonObject } from 'flowise-components' import { ICommonObject, INodeOptionsValue } from 'flowise-components'
import { fork } from 'child_process' import { fork } from 'child_process'
import { Tool } from './entity/Tool'
export class App { export class App {
app: express.Application 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 // Chatflows
// ---------------------------------------- // ----------------------------------------
@ -257,6 +282,63 @@ export class App {
return res.json(results) 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 // Configuration
// ---------------------------------------- // ----------------------------------------
@ -623,6 +705,7 @@ export class App {
this.nodesPool.componentNodes, this.nodesPool.componentNodes,
incomingInput.question, incomingInput.question,
chatId, chatId,
this.AppDataSource,
incomingInput?.overrideConfig incomingInput?.overrideConfig
) )

View File

@ -15,10 +15,15 @@ import {
IOverrideConfig IOverrideConfig
} from '../Interface' } from '../Interface'
import { cloneDeep, get, omit, merge } from 'lodash' 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 { 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' const QUESTION_VAR_PREFIX = 'question'
export const databaseEntities: IDatabaseEntity = { ChatFlow: ChatFlow, ChatMessage: ChatMessage, Tool: Tool }
/** /**
* Returns the home folder path of the user if * Returns the home folder path of the user if
@ -183,6 +188,7 @@ export const buildLangchain = async (
componentNodes: IComponentNodes, componentNodes: IComponentNodes,
question: string, question: string,
chatId: string, chatId: string,
appDataSource: DataSource,
overrideConfig?: ICommonObject overrideConfig?: ICommonObject
) => { ) => {
const flowNodes = cloneDeep(reactFlowNodes) const flowNodes = cloneDeep(reactFlowNodes)
@ -215,7 +221,11 @@ export const buildLangchain = async (
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question) 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) { } catch (e: any) {
console.error(e) console.error(e)
throw new Error(e) throw new Error(e)

View File

@ -13,6 +13,7 @@
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.0.3", "@mui/icons-material": "^5.0.3",
"@mui/material": "^5.11.12", "@mui/material": "^5.11.12",
"@mui/x-data-grid": "^6.8.0",
"@tabler/icons": "^1.39.1", "@tabler/icons": "^1.39.1",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"formik": "^2.2.6", "formik": "^2.2.6",

View File

@ -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
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,8 +1,8 @@
// assets // assets
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons' import { IconHierarchy, IconBuildingStore, IconKey, IconTool } from '@tabler/icons'
// constant // constant
const icons = { IconHierarchy, IconBuildingStore, IconKey } const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool }
// ==============================|| DASHBOARD MENU ITEMS ||============================== // // ==============================|| DASHBOARD MENU ITEMS ||============================== //
@ -27,6 +27,14 @@ const dashboard = {
icon: icons.IconBuildingStore, icon: icons.IconBuildingStore,
breadcrumbs: true breadcrumbs: true
}, },
{
id: 'tools',
title: 'Tools',
type: 'item',
url: '/tools',
icon: icons.IconTool,
breadcrumbs: true
},
{ {
id: 'apikey', id: 'apikey',
title: 'API Keys', title: 'API Keys',

View File

@ -13,6 +13,9 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// apikey routing // apikey routing
const APIKey = Loadable(lazy(() => import('views/apikey'))) const APIKey = Loadable(lazy(() => import('views/apikey')))
// apikey routing
const Tools = Loadable(lazy(() => import('views/tools')))
// ==============================|| MAIN ROUTING ||============================== // // ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = { const MainRoutes = {
@ -34,6 +37,10 @@ const MainRoutes = {
{ {
path: '/apikey', path: '/apikey',
element: <APIKey /> element: <APIKey />
},
{
path: '/tools',
element: <Tools />
} }
] ]
} }

View File

@ -1,8 +1,8 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
// material-ui // material-ui
import { styled, useTheme } from '@mui/material/styles' import { styled } from '@mui/material/styles'
import { Box, Grid, Chip, Typography } from '@mui/material' import { Box, Grid, Typography } from '@mui/material'
// project imports // project imports
import MainCard from 'ui-component/cards/MainCard' import MainCard from 'ui-component/cards/MainCard'
@ -27,20 +27,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
// ===========================|| CONTRACT CARD ||=========================== // // ===========================|| CONTRACT CARD ||=========================== //
const ItemCard = ({ isLoading, data, images, onClick }) => { const ItemCard = ({ isLoading, data, images, color, onClick }) => {
const theme = useTheme()
const chipSX = {
height: 24,
padding: '0 6px'
}
const activeChatflowSX = {
...chipSX,
color: 'white',
backgroundColor: theme.palette.success.dark
}
return ( return (
<> <>
{isLoading ? ( {isLoading ? (
@ -49,7 +36,24 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
<CardWrapper border={false} content={false} onClick={onClick}> <CardWrapper border={false} content={false} onClick={onClick}>
<Box sx={{ p: 2.25 }}> <Box sx={{ p: 2.25 }}>
<Grid container direction='column'> <Grid container direction='column'>
<div> <div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
{color && (
<div
style={{
width: 35,
height: 35,
marginRight: 10,
borderRadius: '50%',
background: color
}}
></div>
)}
<Typography <Typography
sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
> >
@ -61,13 +65,6 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
{data.description} {data.description}
</span> </span>
)} )}
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
{data.deployed && (
<Grid item>
<Chip label='Deployed' sx={activeChatflowSX} />
</Grid>
)}
</Grid>
{images && ( {images && (
<div <div
style={{ style={{
@ -110,6 +107,7 @@ ItemCard.propTypes = {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
data: PropTypes.object, data: PropTypes.object,
images: PropTypes.array, images: PropTypes.array,
color: PropTypes.string,
onClick: PropTypes.func onClick: PropTypes.func
} }

View File

@ -0,0 +1,147 @@
import { useState, useEffect, Fragment } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import axios from 'axios'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
import { baseURL } from 'store/constant'
const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
borderRadius: '10px',
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 10,
margin: 10
}
}
})
const fetchList = async ({ name, nodeData }) => {
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 (
<>
<Autocomplete
id={name}
disabled={disabled}
disableClearable={disableClearable}
size='small'
sx={{ width: '100%' }}
open={open}
onOpen={() => {
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) => (
<TextField
{...params}
value={internalValue}
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{loading ? <CircularProgress color='inherit' size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='h5'>{option.label}</Typography>
{option.description && (
<Typography sx={{ color: customization.isDarkMode ? '#9e9e9e' : '' }}>{option.description}</Typography>
)}
</div>
</Box>
)}
/>
</>
)
}
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
}

View File

@ -21,6 +21,7 @@ export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, sty
onValueChange={onValueChange} onValueChange={onValueChange}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onBlur={onBlur} onBlur={onBlur}
tabSize={4}
style={{ style={{
...style, ...style,
background: theme.palette.codeEditor.main background: theme.palette.codeEditor.main

View File

@ -21,6 +21,7 @@ export const LightCodeEditor = ({ value, placeholder, disabled = false, type, st
onValueChange={onValueChange} onValueChange={onValueChange}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onBlur={onBlur} onBlur={onBlur}
tabSize={4}
style={{ style={{
...style, ...style,
background: theme.palette.card.main background: theme.palette.card.main

View File

@ -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 (
<>
<Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
Add Item
</Button>
{rows && columns && (
<div style={{ marginTop: 10, height: 300, width: '100%', ...style }}>
<DataGrid
processRowUpdate={handleProcessRowUpdate}
onProcessRowUpdateError={(error) => console.error(error)}
rows={rows}
columns={columns}
/>
</div>
)}
</>
)
}
Grid.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
style: PropTypes.any,
addNewRow: PropTypes.func,
onRowUpdate: PropTypes.func
}

View File

@ -39,7 +39,7 @@ export const initNode = (nodeData, newNodeId) => {
const incoming = nodeData.inputs ? nodeData.inputs.length : 0 const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1 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) { for (let i = 0; i < incoming; i += 1) {
const newInput = { 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
}

View File

@ -7,10 +7,11 @@ import { useSelector } from 'react-redux'
import { useTheme, styled } from '@mui/material/styles' import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton } from '@mui/material' import { Box, Typography, Tooltip, IconButton } from '@mui/material'
import { tooltipClasses } from '@mui/material/Tooltip' import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize } from '@tabler/icons' import { IconArrowsMaximize, IconEdit } from '@tabler/icons'
// project import // project import
import { Dropdown } from 'ui-component/dropdown/Dropdown' import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
import { Input } from 'ui-component/input/Input' import { Input } from 'ui-component/input/Input'
import { File } from 'ui-component/file/File' import { File } from 'ui-component/file/File'
import { SwitchInput } from 'ui-component/switch/Switch' import { SwitchInput } from 'ui-component/switch/Switch'
@ -18,6 +19,9 @@ import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper' import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper'
import { JsonEditorInput } from 'ui-component/json/JsonEditor' import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import ToolDialog from 'views/tools/ToolDialog'
const EDITABLE_TOOLS = ['selectedTool']
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({ const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
[`& .${tooltipClasses.tooltip}`]: { [`& .${tooltipClasses.tooltip}`]: {
@ -36,6 +40,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [position, setPosition] = useState(0) const [position, setPosition] = useState(0)
const [showExpandDialog, setShowExpandDialog] = useState(false) const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({}) const [expandDialogProps, setExpandDialogProps] = useState({})
const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('')
const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({})
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const onExpandDialogClicked = (value, inputParam) => { const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = { const dialogProp = {
@ -61,6 +68,42 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
data.inputs[inputParamName] = newValue 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(() => { useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) { if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2) setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
@ -186,12 +229,44 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
name={inputParam.name} name={inputParam.name}
options={inputParam.options} options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} 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 && <div style={{ marginTop: 10 }} />}
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
<AsyncDropdown
disabled={disabled}
name={inputParam.name}
nodeData={data}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
isCreateNewOption={EDITABLE_TOOLS.includes(inputParam.name)}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
onCreateNew={() => addAsyncOption(inputParam.name)}
/>
{EDITABLE_TOOLS.includes(inputParam.name) && data.inputs[inputParam.name] && (
<IconButton
title='Edit'
color='primary'
size='small'
onClick={() => editAsyncOption(inputParam.name, data.inputs[inputParam.name])}
>
<IconEdit />
</IconButton>
)}
</div>
</>
)}
</Box> </Box>
</> </>
)} )}
<ToolDialog
show={EDITABLE_TOOLS.includes(showAsyncOptionDialog)}
dialogProps={asyncOptionEditDialogProps}
onCancel={() => setAsyncOptionEditDialog('')}
onConfirm={onConfirmAsyncOption}
></ToolDialog>
</div> </div>
) )
} }

View File

@ -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&current_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) => [
<GridActionsCellItem key={'Delete'} icon={<DeleteIcon />} 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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='md'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Tool Name
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='toolName'
type='string'
fullWidth
placeholder='My New Tool'
value={toolName}
name='toolName'
onChange={(e) => setToolName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Tool description
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='toolDesc'
type='string'
fullWidth
placeholder='Description of what the tool does. This is for ChatGPT to determine when to use this tool.'
multiline={true}
rows={3}
value={toolDesc}
name='toolDesc'
onChange={(e) => setToolDesc(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Output Schema
<TooltipWithParser style={{ marginLeft: 10 }} title={'What should be the output response in JSON format?'} />
</Typography>
</Stack>
<Grid columns={columns} rows={toolSchema} addNewRow={addNewRow} onRowUpdate={onRowUpdate} />
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Javascript Function
<TooltipWithParser
style={{ marginLeft: 10 }}
title='Function to execute when tool is being used. You can use properties specified in Output Schema as variables. For example, if the property is <code>userid</code>, you can use as <code>$userid</code>. Return value must be a string.'
/>
</Typography>
</Stack>
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
See Example
</Button>
{customization.isDarkMode ? (
<DarkCodeEditor
value={toolFunc}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
borderRadius: 5
}}
/>
) : (
<LightCodeEditor
value={toolFunc}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
border: `1px solid ${theme.palette.grey[300]}`,
borderRadius: 5
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>
{dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteTool()}>
Delete
</StyledButton>
)}
<StyledButton
disabled={!(toolName && toolDesc)}
variant='contained'
onClick={() => (dialogProps.type === 'ADD' ? addNewTool() : saveTool())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
ToolDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default ToolDialog

View File

@ -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 (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Tools</h1>
<Grid sx={{ mb: 1.25 }} container direction='row'>
<Box sx={{ flexGrow: 1 }} />
<Grid item>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
Create New
</StyledButton>
</Grid>
</Grid>
</Stack>
<Grid container spacing={gridSpacing}>
{!getAllToolsApi.loading &&
getAllToolsApi.data &&
getAllToolsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard data={data} color={data.color} onClick={() => edit(data)} />
</Grid>
))}
</Grid>
{!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' />
</Box>
<div>No Tools Created Yet</div>
</Stack>
)}
</MainCard>
<ToolDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={() => setShowDialog(false)}
onConfirm={onConfirm}
></ToolDialog>
</>
)
}
export default Tools