From 0f293e5a598fe5931567911e9427893b50e536f8 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 7 Nov 2023 20:45:25 +0000 Subject: [PATCH] add openai assistant --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 224 +++++++ .../nodes/agents/OpenAIAssistant/openai.png | Bin 0 -> 3991 bytes .../nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 12 + packages/components/package.json | 1 + packages/server/src/Interface.ts | 9 + .../server/src/database/entities/Assistant.ts | 24 + .../server/src/database/entities/index.ts | 4 +- .../mysql/1699325775451-AddAssistantEntity.ts | 21 + .../src/database/migrations/mysql/index.ts | 4 +- .../1699325775451-AddAssistantEntity.ts | 21 + .../src/database/migrations/postgres/index.ts | 4 +- .../1699325775451-AddAssistantEntity.ts | 13 + .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 239 +++++++- packages/server/src/utils/index.ts | 21 +- packages/ui/package.json | 1 + packages/ui/src/api/assistants.js | 25 + packages/ui/src/menu-items/dashboard.js | 12 +- packages/ui/src/routes/MainRoutes.js | 7 + .../ui-component/dialog/ViewMessagesDialog.js | 10 +- .../src/views/assistants/AssistantDialog.js | 545 ++++++++++++++++++ .../views/assistants/LoadAssistantDialog.js | 114 ++++ packages/ui/src/views/assistants/index.js | 146 +++++ .../ui/src/views/chatmessage/ChatMessage.js | 3 +- 24 files changed, 1443 insertions(+), 21 deletions(-) create mode 100644 packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts create mode 100644 packages/components/nodes/agents/OpenAIAssistant/openai.png create mode 100644 packages/server/src/database/entities/Assistant.ts create mode 100644 packages/server/src/database/migrations/mysql/1699325775451-AddAssistantEntity.ts create mode 100644 packages/server/src/database/migrations/postgres/1699325775451-AddAssistantEntity.ts create mode 100644 packages/server/src/database/migrations/sqlite/1699325775451-AddAssistantEntity.ts create mode 100644 packages/ui/src/api/assistants.js create mode 100644 packages/ui/src/views/assistants/AssistantDialog.js create mode 100644 packages/ui/src/views/assistants/LoadAssistantDialog.js create mode 100644 packages/ui/src/views/assistants/index.js diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts new file mode 100644 index 00000000000..03a52560165 --- /dev/null +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -0,0 +1,224 @@ +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' +import OpenAI from 'openai' +import { DataSource } from 'typeorm' +import { getCredentialData, getCredentialParam, getUserHome } from '../../../src/utils' +import { MessageContentImageFile, MessageContentText } from 'openai/resources/beta/threads/messages/messages' +import * as fsDefault from 'node:fs' +import * as path from 'node:path' +import fetch from 'node-fetch' + +class OpenAIAssistant_Agents implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'OpenAI Assistant' + this.name = 'openAIAssistant' + this.version = 1.0 + this.type = 'OpenAIAssistant' + this.category = 'Agents' + this.icon = 'openai.png' + this.description = `An agent that uses OpenAI Assistant API to pick the tool and args to call` + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Select Assistant', + name: 'selectedAssistant', + type: 'asyncOptions', + loadMethod: 'listAssistants' + } + ] + } + + //@ts-ignore + loadMethods = { + async listAssistants(_: 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 assistants = await appDataSource.getRepository(databaseEntities['Assistant']).find() + + for (let i = 0; i < assistants.length; i += 1) { + const assistantDetails = JSON.parse(assistants[i].details) + const data = { + label: assistantDetails.name, + name: assistants[i].id, + description: assistantDetails.instructions + } as INodeOptionsValue + returnData.push(data) + } + return returnData + } + } + + async init(): Promise { + return null + } + + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + let sessionId = nodeData.inputs?.sessionId as string + + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId + }) + + if (!assistant) throw new Error(`Assistant ${selectedAssistantId} not found`) + + if (!sessionId && options.chatId) { + const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId: options.chatId + }) + if (!chatmsg) throw new Error(`Chat Message with Chat Id: ${options.chatId} not found`) + sessionId = chatmsg.sessionId + } + + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) throw new Error(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + await openai.beta.threads.del(sessionId) + options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId + }) + + if (!assistant) throw new Error(`Assistant ${selectedAssistantId} not found`) + + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) throw new Error(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + + // Retrieve assistant + const assistantDetails = JSON.parse(assistant.details) + const openAIAssistantId = assistantDetails.id + const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) + + const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId: options.chatId + }) + + let threadId = '' + if (!chatmessage) { + const thread = await openai.beta.threads.create({}) + threadId = thread.id + } else { + const thread = await openai.beta.threads.retrieve(chatmessage.sessionId) + threadId = thread.id + } + + // Add message to thread + await openai.beta.threads.messages.create(threadId, { + role: 'user', + content: input + }) + + // Run assistant thread + const runThread = await openai.beta.threads.runs.create(threadId, { + assistant_id: retrievedAssistant.id + }) + + const promise = (threadId: string, runId: string) => { + return new Promise((resolve, reject) => { + const timeout = setInterval(async () => { + const run = await openai.beta.threads.runs.retrieve(threadId, runId) + const state = run.status + if (state === 'completed') { + clearInterval(timeout) + resolve(run) + } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { + clearInterval(timeout) + reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) + } + }, 500) + }) + } + + // Polling run status + await promise(threadId, runThread.id) + + // List messages + const messages = await openai.beta.threads.messages.list(threadId) + const messageData = messages.data ?? [] + const assistantMessages = messageData.filter((msg) => msg.role === 'assistant') + if (!assistantMessages.length) return '' + + let returnVal = '' + for (let i = 0; i < assistantMessages[0].content.length; i += 1) { + if (assistantMessages[0].content[i].type === 'text') { + const content = assistantMessages[0].content[i] as MessageContentText + returnVal += content.text.value + + //TODO: handle annotations + } else { + const content = assistantMessages[0].content[i] as MessageContentImageFile + const fileId = content.image_file.file_id + const fileObj = await openai.files.retrieve(fileId) + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', `${fileObj.filename}.png`) + + await downloadFile(fileObj, filePath, openAIApiKey) + + const bitmap = fsDefault.readFileSync(filePath) + const base64String = Buffer.from(bitmap).toString('base64') + + const imgHTML = `${fileObj.filename}
` + returnVal += imgHTML + } + } + + return { text: returnVal, assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData } } + } +} + +const downloadFile = async (fileObj: any, filePath: string, openAIApiKey: string) => { + try { + const response = await fetch(`https://api.openai.com/v1/files/${fileObj.id}/content`, { + method: 'GET', + headers: { Accept: '*/*', Authorization: `Bearer ${openAIApiKey}` } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + await new Promise((resolve, reject) => { + const dest = fsDefault.createWriteStream(filePath) + response.body.pipe(dest) + response.body.on('end', () => resolve()) + dest.on('error', reject) + }) + + // eslint-disable-next-line no-console + console.log('File downloaded and written to', filePath) + } catch (error) { + console.error('Error downloading or writing the file:', error) + } +} + +module.exports = { nodeClass: OpenAIAssistant_Agents } diff --git a/packages/components/nodes/agents/OpenAIAssistant/openai.png b/packages/components/nodes/agents/OpenAIAssistant/openai.png new file mode 100644 index 0000000000000000000000000000000000000000..de08a05b28979826c4cc669c4899789763a938a1 GIT binary patch literal 3991 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 literal 0 HcmV?d00001 diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index f74bd642f9a..32ff5d64b00 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -47,6 +47,14 @@ class ChatOpenAI_ChatModels implements INode { label: 'gpt-4', name: 'gpt-4' }, + { + label: 'gpt-4-1106-preview', + name: 'gpt-4-1106-preview' + }, + { + label: 'gpt-4-vision-preview', + name: 'gpt-4-vision-preview' + }, { label: 'gpt-4-0613', name: 'gpt-4-0613' @@ -63,6 +71,10 @@ class ChatOpenAI_ChatModels implements INode { label: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo' }, + { + label: 'gpt-3.5-turbo-1106', + name: 'gpt-3.5-turbo-1106' + }, { label: 'gpt-3.5-turbo-0613', name: 'gpt-3.5-turbo-0613' diff --git a/packages/components/package.json b/packages/components/package.json index 051eec2f512..cc2e5227378 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -60,6 +60,7 @@ "node-html-markdown": "^1.3.0", "notion-to-md": "^3.1.1", "object-hash": "^3.0.0", + "openai": "^4.16.1", "pdf-parse": "^1.1.1", "pdfjs-dist": "^3.7.107", "pg": "^8.11.2", diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index d6d24596d0d..1a5a694f6f3 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -48,6 +48,15 @@ export interface ITool { createdDate: Date } +export interface IAssistant { + id: string + details: string + credential: string + iconSrc?: string + updatedDate: Date + createdDate: Date +} + export interface ICredential { id: string name: string diff --git a/packages/server/src/database/entities/Assistant.ts b/packages/server/src/database/entities/Assistant.ts new file mode 100644 index 00000000000..1a6eeec6c21 --- /dev/null +++ b/packages/server/src/database/entities/Assistant.ts @@ -0,0 +1,24 @@ +/* eslint-disable */ +import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm' +import { IAssistant } from '../../Interface' + +@Entity() +export class Assistant implements IAssistant { + @PrimaryGeneratedColumn('uuid') + id: string + + @Column({ type: 'text' }) + details: string + + @Column() + credential: string + + @Column({ nullable: true }) + iconSrc?: string + + @CreateDateColumn() + createdDate: Date + + @UpdateDateColumn() + updatedDate: Date +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index ff1098632ee..58447a1f533 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -2,10 +2,12 @@ import { ChatFlow } from './ChatFlow' import { ChatMessage } from './ChatMessage' import { Credential } from './Credential' import { Tool } from './Tool' +import { Assistant } from './Assistant' export const entities = { ChatFlow, ChatMessage, Credential, - Tool + Tool, + Assistant } diff --git a/packages/server/src/database/migrations/mysql/1699325775451-AddAssistantEntity.ts b/packages/server/src/database/migrations/mysql/1699325775451-AddAssistantEntity.ts new file mode 100644 index 00000000000..0ce66943045 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1699325775451-AddAssistantEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAssistantEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`assistant\` ( + \`id\` varchar(36) NOT NULL, + \`credential\` varchar(255) NOT NULL, + \`details\` text NOT NULL, + \`iconSrc\` varchar(255) DEFAULT NULL, + \`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE assistant`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index aa34fa552f1..e372698c604 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -6,6 +6,7 @@ import { ModifyTool1694001465232 } from './1694001465232-ModifyTool' import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' +import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' export const mysqlMigrations = [ Init1693840429259, @@ -15,5 +16,6 @@ export const mysqlMigrations = [ ModifyTool1694001465232, AddApiConfig1694099200729, AddAnalytic1694432361423, - AddChatHistory1694658767766 + AddChatHistory1694658767766, + AddAssistantEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/postgres/1699325775451-AddAssistantEntity.ts b/packages/server/src/database/migrations/postgres/1699325775451-AddAssistantEntity.ts new file mode 100644 index 00000000000..b3cd4715a42 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1699325775451-AddAssistantEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAssistantEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS assistant ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + "credential" varchar NOT NULL, + "details" text NOT NULL, + "iconSrc" varchar NULL, + "createdDate" timestamp NOT NULL DEFAULT now(), + "updatedDate" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_3c7cea7a044ac4c92764576cdbf" PRIMARY KEY (id) + );` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE assistant`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index e16d9107b46..75a1e5239e9 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -6,6 +6,7 @@ import { ModifyTool1693997339912 } from './1693997339912-ModifyTool' import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' +import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' export const postgresMigrations = [ Init1693891895163, @@ -15,5 +16,6 @@ export const postgresMigrations = [ ModifyTool1693997339912, AddApiConfig1694099183389, AddAnalytic1694432361423, - AddChatHistory1694658756136 + AddChatHistory1694658756136, + AddAssistantEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/sqlite/1699325775451-AddAssistantEntity.ts b/packages/server/src/database/migrations/sqlite/1699325775451-AddAssistantEntity.ts new file mode 100644 index 00000000000..b5257ac453a --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1699325775451-AddAssistantEntity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAssistantEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "assistant" ("id" varchar PRIMARY KEY NOT NULL, "details" text NOT NULL, "credential" varchar NOT NULL, "iconSrc" varchar, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "updatedDate" datetime NOT NULL DEFAULT (datetime('now')));` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE assistant`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 680f762b390..ca4ecd57c73 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -6,6 +6,7 @@ import { ModifyTool1693924207475 } from './1693924207475-ModifyTool' import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' +import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' export const sqliteMigrations = [ Init1693835579790, @@ -15,5 +16,6 @@ export const sqliteMigrations = [ ModifyTool1693924207475, AddApiConfig1694090982460, AddAnalytic1694432361423, - AddChatHistory1694657778173 + AddChatHistory1694657778173, + AddAssistantEntity1699325775451 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index eade3d981a1..c9b252488e8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -9,6 +9,7 @@ import { Server } from 'socket.io' import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' import { v4 as uuidv4 } from 'uuid' +import OpenAI from 'openai' import { Between, IsNull, FindOptionsWhere } from 'typeorm' import { IChatFlow, @@ -57,6 +58,7 @@ import { ChatFlow } from './database/entities/ChatFlow' import { ChatMessage } from './database/entities/ChatMessage' import { Credential } from './database/entities/Credential' import { Tool } from './database/entities/Tool' +import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' @@ -469,8 +471,8 @@ export class App { const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - if (isClearFromViewMessageDialog) - clearSessionMemoryFromViewMessageDialog( + if (isClearFromViewMessageDialog) { + await clearSessionMemoryFromViewMessageDialog( nodes, this.nodesPool.componentNodes, chatId, @@ -478,7 +480,9 @@ export class App { sessionId, memoryType ) - else clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) + } else { + await clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) + } const deleteOptions: FindOptionsWhere = { chatflowid, chatId } if (memoryType) deleteOptions.memoryType = memoryType @@ -631,6 +635,224 @@ export class App { return res.json(results) }) + // ---------------------------------------- + // Assistant + // ---------------------------------------- + + // Get all assistants + this.app.get('/api/v1/assistants', async (req: Request, res: Response) => { + const assistants = await this.AppDataSource.getRepository(Assistant).find() + return res.json(assistants) + }) + + // Get specific assistant + this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { + const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ + id: req.params.id + }) + return res.json(assistant) + }) + + // Get assistant object + this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + + return res.json(retrievedAssistant) + }) + + // List available assistants + this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistants = await openai.beta.assistants.list() + + return res.json(retrievedAssistants.data) + }) + + // Add assistant + this.app.post('/api/v1/assistants', async (req: Request, res: Response) => { + const body = req.body + + if (!body.details) return res.status(500).send(`Invalid request body`) + + const assistantDetails = JSON.parse(body.details) + + if (!assistantDetails.id) { + try { + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: body.credential + }) + + if (!credential) return res.status(404).send(`Credential ${body.credential} not found`) + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + + let tools = [] + if (assistantDetails.tools) { + for (const tool of assistantDetails.tools ?? []) { + tools.push({ + type: tool + }) + } + } + const newAssistant = await openai.beta.assistants.create({ + name: assistantDetails.name, + description: assistantDetails.description, + instructions: assistantDetails.instructions, + model: assistantDetails.model, + tools + }) + + const newAssistantDetails = { + ...assistantDetails, + id: newAssistant.id + } + + body.details = JSON.stringify(newAssistantDetails) + } catch (error) { + return res.status(500).send(`Error creating new assistant: ${error}`) + } + } + + const newAssistant = new Assistant() + Object.assign(newAssistant, body) + + const assistant = this.AppDataSource.getRepository(Assistant).create(newAssistant) + const results = await this.AppDataSource.getRepository(Assistant).save(assistant) + + return res.json(results) + }) + + // Update assistant + this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { + const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ + id: req.params.id + }) + + if (!assistant) { + res.status(404).send(`Assistant ${req.params.id} not found`) + return + } + + try { + const openAIAssistantId = JSON.parse(assistant.details)?.id + + const body = req.body + const assistantDetails = JSON.parse(body.details) + + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: body.credential + }) + + if (!credential) return res.status(404).send(`Credential ${body.credential} not found`) + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + + let tools = [] + if (assistantDetails.tools) { + for (const tool of assistantDetails.tools ?? []) { + tools.push({ + type: tool + }) + } + } + await openai.beta.assistants.update(openAIAssistantId, { + name: assistantDetails.name, + description: assistantDetails.description, + instructions: assistantDetails.instructions, + model: assistantDetails.model, + tools + }) + + const newAssistantDetails = { + ...assistantDetails, + id: openAIAssistantId + } + + const updateAssistant = new Assistant() + body.details = JSON.stringify(newAssistantDetails) + Object.assign(updateAssistant, body) + + this.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant) + const result = await this.AppDataSource.getRepository(Assistant).save(assistant) + + return res.json(result) + } catch (error) { + return res.status(500).send(`Error updating assistant: ${error}`) + } + }) + + // Delete assistant + this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { + const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ + id: req.params.id + }) + + if (!assistant) { + res.status(404).send(`Assistant ${req.params.id} not found`) + return + } + + try { + const body = req.body + const assistantDetails = JSON.parse(body.details) + + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: body.credential + }) + + if (!credential) return res.status(404).send(`Credential ${body.credential} not found`) + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + + const openai = new OpenAI({ apiKey: openAIApiKey }) + + await openai.beta.assistants.del(assistantDetails.id) + + const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id }) + return res.json(results) + } catch (error) { + return res.status(500).send(`Error deleting assistant: ${error}`) + } + }) + // ---------------------------------------- // Configuration // ---------------------------------------- @@ -1121,18 +1343,25 @@ export class App { logger, appDataSource: this.AppDataSource, databaseEntities, - analytic: chatflow.analytic + analytic: chatflow.analytic, + chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, - analytic: chatflow.analytic + analytic: chatflow.analytic, + chatId }) result = typeof result === 'string' ? { text: result } : result + // Retrieve threadId from assistant if exists + if (typeof result === 'object' && result.assistant) { + sessionId = result.assistant.threadId + } + const userMessage: Omit = { role: 'userMessage', content: incomingInput.question, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 96086efd7db..239773a9a9b 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -34,6 +34,7 @@ import { ChatFlow } from '../database/entities/ChatFlow' import { ChatMessage } from '../database/entities/ChatMessage' import { Credential } from '../database/entities/Credential' import { Tool } from '../database/entities/Tool' +import { Assistant } from '../database/entities/Assistant' import { DataSource } from 'typeorm' import { CachePool } from '../CachePool' @@ -41,7 +42,13 @@ const QUESTION_VAR_PREFIX = 'question' const CHAT_HISTORY_VAR_PREFIX = 'chat_history' const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db' -export const databaseEntities: IDatabaseEntity = { ChatFlow: ChatFlow, ChatMessage: ChatMessage, Tool: Tool, Credential: Credential } +export const databaseEntities: IDatabaseEntity = { + ChatFlow: ChatFlow, + ChatMessage: ChatMessage, + Tool: Tool, + Credential: Credential, + Assistant: Assistant +} /** * Returns the home folder path of the user if @@ -313,12 +320,14 @@ export const clearAllSessionMemory = async ( sessionId?: string ) => { for (const node of reactFlowNodes) { - if (node.data.category !== 'Memory') continue + if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() - if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId + if (sessionId && node.data.inputs) { + node.data.inputs.sessionId = sessionId + } if (newNodeInstance.clearSessionMemory) { await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) @@ -345,8 +354,8 @@ export const clearSessionMemoryFromViewMessageDialog = async ( ) => { if (!sessionId) return for (const node of reactFlowNodes) { - if (node.data.category !== 'Memory') continue - if (node.data.label !== memoryType) continue + if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue + if (memoryType && node.data.label !== memoryType) continue const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() @@ -912,6 +921,8 @@ export const decryptCredentialData = async ( ): Promise => { const encryptKey = await getEncryptionKey() const decryptedData = AES.decrypt(encryptedData, encryptKey) + const decryptedDataStr = decryptedData.toString(enc.Utf8) + if (!decryptedDataStr) return {} try { if (componentCredentialName && componentCredentials) { const plainDataObj = JSON.parse(decryptedData.toString(enc.Utf8)) diff --git a/packages/ui/package.json b/packages/ui/package.json index f0a60f5b083..7205f33cf24 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,6 +44,7 @@ "reactflow": "^11.5.6", "redux": "^4.0.5", "rehype-mathjax": "^4.0.2", + "rehype-raw": "^7.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "socket.io-client": "^4.6.1", diff --git a/packages/ui/src/api/assistants.js b/packages/ui/src/api/assistants.js new file mode 100644 index 00000000000..63dd5e18ab2 --- /dev/null +++ b/packages/ui/src/api/assistants.js @@ -0,0 +1,25 @@ +import client from './client' + +const getAllAssistants = () => client.get('/assistants') + +const getSpecificAssistant = (id) => client.get(`/assistants/${id}`) + +const getAssistantObj = (id, credential) => client.get(`/openai-assistants/${id}?credential=${credential}`) + +const getAllAvailableAssistants = (credential) => client.get(`/openai-assistants?credential=${credential}`) + +const createNewAssistant = (body) => client.post(`/assistants`, body) + +const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body) + +const deleteAssistant = (id) => client.delete(`/assistants/${id}`) + +export default { + getAllAssistants, + getSpecificAssistant, + getAssistantObj, + getAllAvailableAssistants, + createNewAssistant, + updateAssistant, + deleteAssistant +} diff --git a/packages/ui/src/menu-items/dashboard.js b/packages/ui/src/menu-items/dashboard.js index 87ef88f98ac..8bf5b3924a6 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, IconTool, IconLock } from '@tabler/icons' +import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons' // constant -const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock } +const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } // ==============================|| DASHBOARD MENU ITEMS ||============================== // @@ -35,6 +35,14 @@ const dashboard = { icon: icons.IconTool, breadcrumbs: true }, + { + id: 'assistants', + title: 'Assistants', + type: 'item', + url: '/assistants', + icon: icons.IconRobot, + breadcrumbs: true + }, { id: 'credentials', title: 'Credentials', diff --git a/packages/ui/src/routes/MainRoutes.js b/packages/ui/src/routes/MainRoutes.js index 9a1c29affd6..bce0de13754 100644 --- a/packages/ui/src/routes/MainRoutes.js +++ b/packages/ui/src/routes/MainRoutes.js @@ -16,6 +16,9 @@ const APIKey = Loadable(lazy(() => import('views/apikey'))) // tools routing const Tools = Loadable(lazy(() => import('views/tools'))) +// assistants routing +const Assistants = Loadable(lazy(() => import('views/assistants'))) + // credentials routing const Credentials = Loadable(lazy(() => import('views/credentials'))) @@ -45,6 +48,10 @@ const MainRoutes = { path: '/tools', element: }, + { + path: '/assistants', + element: + }, { path: '/credentials', element: diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 6d147dee909..c0cafbe559c 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -4,6 +4,7 @@ import { useState, useEffect, forwardRef } from 'react' import PropTypes from 'prop-types' import moment from 'moment' import rehypeMathjax from 'rehype-mathjax' +import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' @@ -263,9 +264,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } const transformChatPKToParams = (chatPK) => { - const chatId = chatPK.split('_')[0] - const memoryType = chatPK.split('_')[1] - const sessionId = chatPK.split('_')[2] + let [c1, c2, ...rest] = chatPK.split('_') + const chatId = c1 + const memoryType = c2 + const sessionId = rest.join('_') const params = { chatId } if (memoryType !== 'null') params.memoryType = memoryType @@ -601,7 +603,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {/* Messages are being rendered in Markdown format */} { + const portalElement = document.getElementById('portal') + + const dispatch = useDispatch() + + // ==============================|| Snackbar ||============================== // + + useNotifier() + const { confirm } = useConfirm() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const getSpecificAssistantApi = useApi(assistantsApi.getSpecificAssistant) + const getAssistantObjApi = useApi(assistantsApi.getAssistantObj) + + const [assistantId, setAssistantId] = useState('') + const [openAIAssistantId, setOpenAIAssistantId] = useState('') + const [assistantName, setAssistantName] = useState('') + const [assistantDesc, setAssistantDesc] = useState('') + const [assistantIcon, setAssistantIcon] = useState(`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`) + const [assistantModel, setAssistantModel] = useState('') + const [assistantCredential, setAssistantCredential] = useState('') + const [assistantInstructions, setAssistantInstructions] = useState('') + const [assistantTools, setAssistantTools] = useState(['code_interpreter', 'retrieval']) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + useEffect(() => { + if (getSpecificAssistantApi.data) { + setAssistantId(getSpecificAssistantApi.data.id) + setAssistantIcon(getSpecificAssistantApi.data.iconSrc) + setAssistantCredential(getSpecificAssistantApi.data.credential) + + const assistantDetails = JSON.parse(getSpecificAssistantApi.data.details) + setOpenAIAssistantId(assistantDetails.id) + setAssistantName(assistantDetails.name) + setAssistantDesc(assistantDetails.description) + setAssistantModel(assistantDetails.model) + setAssistantInstructions(assistantDetails.instructions) + setAssistantTools(assistantDetails.tools ?? []) + } + }, [getSpecificAssistantApi.data]) + + useEffect(() => { + if (getAssistantObjApi.data) { + setOpenAIAssistantId(getAssistantObjApi.data.id) + setAssistantName(getAssistantObjApi.data.name) + setAssistantDesc(getAssistantObjApi.data.description) + setAssistantModel(getAssistantObjApi.data.model) + setAssistantInstructions(getAssistantObjApi.data.instructions) + + let tools = [] + if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) { + for (const tool of getAssistantObjApi.data.tools) { + tools.push(tool.type) + } + } + setAssistantTools(tools) + } + }, [getAssistantObjApi.data]) + + useEffect(() => { + if (dialogProps.type === 'EDIT' && dialogProps.data) { + // When assistant dialog is opened from Assistants dashboard + setAssistantId(dialogProps.data.id) + setAssistantIcon(dialogProps.data.iconSrc) + setAssistantCredential(dialogProps.data.credential) + + const assistantDetails = JSON.parse(dialogProps.data.details) + setOpenAIAssistantId(assistantDetails.id) + setAssistantName(assistantDetails.name) + setAssistantDesc(assistantDetails.description) + setAssistantModel(assistantDetails.model) + setAssistantInstructions(assistantDetails.instructions) + setAssistantTools(assistantDetails.tools ?? []) + } else if (dialogProps.type === 'EDIT' && dialogProps.assistantId) { + // When assistant dialog is opened from OpenAIAssistant node in canvas + getSpecificAssistantApi.request(dialogProps.assistantId) + } else if (dialogProps.type === 'ADD' && dialogProps.selectedOpenAIAssistantId && dialogProps.credential) { + // When assistant dialog is to add new assistant from existing + setAssistantId('') + setAssistantIcon(`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`) + setAssistantCredential(dialogProps.credential) + + getAssistantObjApi.request(dialogProps.selectedOpenAIAssistantId, dialogProps.credential) + } else if (dialogProps.type === 'ADD' && !dialogProps.selectedOpenAIAssistantId) { + // When assistant dialog is to add a blank new assistant + setAssistantId('') + setAssistantIcon(`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`) + setAssistantCredential('') + + setOpenAIAssistantId('') + setAssistantName('') + setAssistantDesc('') + setAssistantModel('') + setAssistantInstructions('') + setAssistantTools(['code_interpreter', 'retrieval']) + } + + return () => { + setAssistantId('') + setAssistantIcon(`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`) + setAssistantCredential('') + + setOpenAIAssistantId('') + setAssistantName('') + setAssistantDesc('') + setAssistantModel('') + setAssistantInstructions('') + setAssistantTools(['code_interpreter', 'retrieval']) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + const addNewAssistant = async () => { + try { + const assistantDetails = { + id: openAIAssistantId, + name: assistantName, + description: assistantDesc, + model: assistantModel, + instructions: assistantInstructions, + tools: assistantTools + } + const obj = { + details: JSON.stringify(assistantDetails), + iconSrc: assistantIcon, + credential: assistantCredential + } + + const createResp = await assistantsApi.createNewAssistant(obj) + if (createResp.data) { + enqueueSnackbar({ + message: 'New Assistant 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 Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const saveAssistant = async () => { + try { + const assistantDetails = { + name: assistantName, + description: assistantDesc, + model: assistantModel, + instructions: assistantInstructions, + tools: assistantTools + } + const obj = { + details: JSON.stringify(assistantDetails), + iconSrc: assistantIcon, + credential: assistantCredential + } + const saveResp = await assistantsApi.updateAssistant(assistantId, obj) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Assistant saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(saveResp.data.id) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const deleteAssistant = async () => { + const confirmPayload = { + title: `Delete Assistant`, + description: `Delete Assistant ${assistantName}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + const delResp = await assistantsApi.deleteAssistant(assistantId) + if (delResp.data) { + enqueueSnackbar({ + message: 'Assistant 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 Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + } + + const component = show ? ( + + + {dialogProps.title} + + + + + + Assistant Name + + + + setAssistantName(e.target.value)} + /> + + + + + Assistant Description + + + + setAssistantDesc(e.target.value)} + /> + + + + Assistant Icon Src + +
+ {assistantName} +
+ setAssistantIcon(e.target.value)} + /> +
+ + + + Assistant Model +  * + + + setAssistantModel(newValue)} + value={assistantModel ?? 'choose an option'} + /> + + + + + OpenAI Credential +  * + + + setAssistantCredential(newValue)} + /> + + + + + Assistant Instruction + + + + setAssistantInstructions(e.target.value)} + /> + + + + + Assistant Tools + + + + (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))} + value={assistantTools ?? 'choose an option'} + /> + +
+ + {dialogProps.type === 'EDIT' && ( + deleteAssistant()}> + Delete + + )} + (dialogProps.type === 'ADD' ? addNewAssistant() : saveAssistant())} + > + {dialogProps.confirmButtonName} + + + +
+ ) : null + + return createPortal(component, portalElement) +} + +AssistantDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default AssistantDialog diff --git a/packages/ui/src/views/assistants/LoadAssistantDialog.js b/packages/ui/src/views/assistants/LoadAssistantDialog.js new file mode 100644 index 00000000000..d149b21f093 --- /dev/null +++ b/packages/ui/src/views/assistants/LoadAssistantDialog.js @@ -0,0 +1,114 @@ +import { useState, useEffect } from 'react' +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Stack, Typography, Dialog, DialogContent, DialogTitle, DialogActions, Box } from '@mui/material' +import CredentialInputHandler from 'views/canvas/CredentialInputHandler' +import { Dropdown } from 'ui-component/dropdown/Dropdown' +import { StyledButton } from 'ui-component/button/StyledButton' +import assistantsApi from 'api/assistants' +import useApi from 'hooks/useApi' + +const LoadAssistantDialog = ({ show, dialogProps, onCancel, onAssistantSelected }) => { + const portalElement = document.getElementById('portal') + + const getAllAvailableAssistantsApi = useApi(assistantsApi.getAllAvailableAssistants) + + const [credentialId, setCredentialId] = useState('') + const [availableAssistantsOptions, setAvailableAssistantsOptions] = useState([]) + const [selectedOpenAIAssistantId, setSelectedOpenAIAssistantId] = useState('') + + useEffect(() => { + return () => { + setCredentialId('') + setAvailableAssistantsOptions([]) + setSelectedOpenAIAssistantId('') + } + }, [dialogProps]) + + useEffect(() => { + if (getAllAvailableAssistantsApi.data && getAllAvailableAssistantsApi.data.length) { + const assistants = [] + for (let i = 0; i < getAllAvailableAssistantsApi.data.length; i += 1) { + assistants.push({ + label: getAllAvailableAssistantsApi.data[i].name, + name: getAllAvailableAssistantsApi.data[i].id, + description: getAllAvailableAssistantsApi.data[i].instructions + }) + } + setAvailableAssistantsOptions(assistants) + } + }, [getAllAvailableAssistantsApi.data]) + + const component = show ? ( + + + {dialogProps.title} + + + + + + OpenAI Credential +  * + + + { + setCredentialId(newValue) + if (newValue) getAllAvailableAssistantsApi.request(newValue) + }} + /> + + {credentialId && ( + + + + Assistants +  * + + + setSelectedOpenAIAssistantId(newValue)} + value={selectedOpenAIAssistantId ?? 'choose an option'} + /> + + )} + + {selectedOpenAIAssistantId && ( + + onAssistantSelected(selectedOpenAIAssistantId, credentialId)}> + Load + + + )} + + ) : null + + return createPortal(component, portalElement) +} + +LoadAssistantDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onAssistantSelected: PropTypes.func +} + +export default LoadAssistantDialog diff --git a/packages/ui/src/views/assistants/index.js b/packages/ui/src/views/assistants/index.js new file mode 100644 index 00000000000..209f8946d75 --- /dev/null +++ b/packages/ui/src/views/assistants/index.js @@ -0,0 +1,146 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +// material-ui +import { Grid, Box, Stack, Button } 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 AssistantDialog from './AssistantDialog' +import LoadAssistantDialog from './LoadAssistantDialog' + +// API +import assistantsApi from 'api/assistants' + +// Hooks +import useApi from 'hooks/useApi' + +// icons +import { IconPlus, IconFileImport } from '@tabler/icons' + +// ==============================|| CHATFLOWS ||============================== // + +const Assistants = () => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const getAllAssistantsApi = useApi(assistantsApi.getAllAssistants) + + const [showDialog, setShowDialog] = useState(false) + const [dialogProps, setDialogProps] = useState({}) + const [showLoadDialog, setShowLoadDialog] = useState(false) + const [loadDialogProps, setLoadDialogProps] = useState({}) + + const loadExisting = () => { + const dialogProp = { + title: 'Load Existing Assistant' + } + setLoadDialogProps(dialogProp) + setShowLoadDialog(true) + } + + const onAssistantSelected = (selectedOpenAIAssistantId, credential) => { + setShowLoadDialog(false) + addNew(selectedOpenAIAssistantId, credential) + } + + const addNew = (selectedOpenAIAssistantId, credential) => { + const dialogProp = { + title: 'Add New Assistant', + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Add', + selectedOpenAIAssistantId, + credential + } + setDialogProps(dialogProp) + setShowDialog(true) + } + + const edit = (selectedAssistant) => { + const dialogProp = { + title: 'Edit Assistant', + type: 'EDIT', + cancelButtonName: 'Cancel', + confirmButtonName: 'Save', + data: selectedAssistant + } + setDialogProps(dialogProp) + setShowDialog(true) + } + + const onConfirm = () => { + setShowDialog(false) + getAllAssistantsApi.request() + } + + useEffect(() => { + getAllAssistantsApi.request() + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + <> + + + +

OpenAI Assistants

+ + + + }> + Add + + +
+
+ + {!getAllAssistantsApi.loading && + getAllAssistantsApi.data && + getAllAssistantsApi.data.map((data, index) => ( + + edit(data)} + /> + + ))} + + {!getAllAssistantsApi.loading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( + + + ToolEmptySVG + +
No Assistants Added Yet
+
+ )} +
+ setShowLoadDialog(false)} + onAssistantSelected={onAssistantSelected} + > + setShowDialog(false)} + onConfirm={onConfirm} + > + + ) +} + +export default Assistants diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 6d99768af03..3b9be768857 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types' import socketIOClient from 'socket.io-client' import { cloneDeep } from 'lodash' import rehypeMathjax from 'rehype-mathjax' +import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' @@ -287,7 +288,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {/* Messages are being rendered in Markdown format */}