add openai assistant

This commit is contained in:
Henry 2023-11-07 20:45:25 +00:00
parent 12fb5a3a3b
commit 0f293e5a59
24 changed files with 1443 additions and 21 deletions

View File

@ -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<INodeOptionsValue[]> {
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<any> {
return null
}
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
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<string | object> {
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 = `<img src="data:image/png;base64,${base64String}" width="100%" height="max-content" alt="${fileObj.filename}" /><br/>`
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<void>((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 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAssistantEntity1699325775451 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE assistant`)
}
}

View File

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

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAssistantEntity1699325775451 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE assistant`)
}
}

View File

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

View File

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAssistantEntity1699325775451 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE assistant`)
}
}

View File

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

View File

@ -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<ChatMessage> = { 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<IChatMessage, 'id'> = {
role: 'userMessage',
content: incomingInput.question,

View File

@ -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<ICredentialDataDecrypted> => {
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))

View File

@ -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",

View File

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

View File

@ -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',

View File

@ -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: <Tools />
},
{
path: '/assistants',
element: <Assistants />
},
{
path: '/credentials',
element: <Credentials />

View File

@ -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 */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')

View File

@ -0,0 +1,545 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import { v4 as uuidv4 } from 'uuid'
import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
// Icons
import { IconX } from '@tabler/icons'
// API
import assistantsApi from 'api/assistants'
// Hooks
import useConfirm from 'hooks/useConfirm'
import useApi from 'hooks/useApi'
// utils
import useNotifier from 'utils/useNotifier'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const assistantAvailableModels = [
{
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'
},
{
label: 'gpt-4-32k',
name: 'gpt-4-32k'
},
{
label: 'gpt-4-32k-0613',
name: 'gpt-4-32k-0613'
},
{
label: 'gpt-3.5-turbo',
name: 'gpt-3.5-turbo'
},
{
label: 'gpt-3.5-turbo-1106',
name: 'gpt-3.5-turbo-1106'
},
{
label: 'gpt-3.5-turbo-0613',
name: 'gpt-3.5-turbo-0613'
},
{
label: 'gpt-3.5-turbo-16k',
name: 'gpt-3.5-turbo-16k'
},
{
label: 'gpt-3.5-turbo-16k-0613',
name: 'gpt-3.5-turbo-16k-0613'
}
]
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
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) => (
<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 Assistant: ${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 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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<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 Assistant: ${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'>
Assistant Name
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'The name of the assistant. The maximum length is 256 characters.'}
/>
</Typography>
</Stack>
<OutlinedInput
id='assistantName'
type='string'
fullWidth
placeholder='My New Assistant'
value={assistantName}
name='assistantName'
onChange={(e) => setAssistantName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Description
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'The description of the assistant. The maximum length is 512 characters.'}
/>
</Typography>
</Stack>
<OutlinedInput
id='assistantDesc'
type='string'
fullWidth
placeholder='Description of what the Assistant does'
multiline={true}
rows={3}
value={assistantDesc}
name='assistantDesc'
onChange={(e) => setAssistantDesc(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Assistant Icon Src</Typography>
</Stack>
<div
style={{
width: 100,
height: 100,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={assistantName}
src={assistantIcon}
/>
</div>
<OutlinedInput
id='assistantIcon'
type='string'
fullWidth
placeholder={`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`}
value={assistantIcon}
name='assistantIcon'
onChange={(e) => setAssistantIcon(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Model
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
key={assistantModel}
name={assistantModel}
options={assistantAvailableModels}
onSelect={(newValue) => setAssistantModel(newValue)}
value={assistantModel ?? 'choose an option'}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
OpenAI Credential
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<CredentialInputHandler
key={assistantCredential}
data={assistantCredential ? { credential: assistantCredential } : {}}
inputParam={{
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}}
onSelect={(newValue) => setAssistantCredential(newValue)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Instruction
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'The system instructions that the assistant uses. The maximum length is 32768 characters.'}
/>
</Typography>
</Stack>
<OutlinedInput
id='assistantInstructions'
type='string'
fullWidth
placeholder='You are a personal math tutor. When asked a question, write and run Python code to answer the question.'
multiline={true}
rows={3}
value={assistantInstructions}
name='assistantInstructions'
onChange={(e) => setAssistantInstructions(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Tools
<TooltipWithParser
style={{ marginLeft: 10 }}
title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.'
/>
</Typography>
</Stack>
<MultiDropdown
key={JSON.stringify(assistantTools)}
name={JSON.stringify(assistantTools)}
options={[
{
label: 'Code Interpreter',
name: 'code_interpreter'
},
{
label: 'Retrieval',
name: 'retrieval'
}
]}
onSelect={(newValue) => (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))}
value={assistantTools ?? 'choose an option'}
/>
</Box>
</DialogContent>
<DialogActions>
{dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteAssistant()}>
Delete
</StyledButton>
)}
<StyledButton
disabled={!(assistantModel && assistantCredential)}
variant='contained'
onClick={() => (dialogProps.type === 'ADD' ? addNewAssistant() : saveAssistant())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AssistantDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AssistantDialog

View File

@ -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 ? (
<Dialog
fullWidth
maxWidth='xs'
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'>
OpenAI Credential
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<CredentialInputHandler
key={credentialId}
data={credentialId ? { credential: credentialId } : {}}
inputParam={{
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}}
onSelect={(newValue) => {
setCredentialId(newValue)
if (newValue) getAllAvailableAssistantsApi.request(newValue)
}}
/>
</Box>
{credentialId && (
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistants
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
name={selectedOpenAIAssistantId}
options={availableAssistantsOptions}
onSelect={(newValue) => setSelectedOpenAIAssistantId(newValue)}
value={selectedOpenAIAssistantId ?? 'choose an option'}
/>
</Box>
)}
</DialogContent>
{selectedOpenAIAssistantId && (
<DialogActions>
<StyledButton variant='contained' onClick={() => onAssistantSelected(selectedOpenAIAssistantId, credentialId)}>
Load
</StyledButton>
</DialogActions>
)}
</Dialog>
) : null
return createPortal(component, portalElement)
}
LoadAssistantDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onAssistantSelected: PropTypes.func
}
export default LoadAssistantDialog

View File

@ -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 (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<Grid sx={{ mb: 1.25 }} container direction='row'>
<h1>OpenAI Assistants</h1>
<Box sx={{ flexGrow: 1 }} />
<Grid item>
<Button variant='outlined' sx={{ mr: 2 }} onClick={loadExisting} startIcon={<IconFileImport />}>
Load
</Button>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
Add
</StyledButton>
</Grid>
</Grid>
</Stack>
<Grid container spacing={gridSpacing}>
{!getAllAssistantsApi.loading &&
getAllAssistantsApi.data &&
getAllAssistantsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard
data={{
name: JSON.parse(data.details)?.name,
description: JSON.parse(data.details)?.instructions,
iconSrc: data.iconSrc
}}
onClick={() => edit(data)}
/>
</Grid>
))}
</Grid>
{!getAllAssistantsApi.loading && (!getAllAssistantsApi.data || getAllAssistantsApi.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 Assistants Added Yet</div>
</Stack>
)}
</MainCard>
<LoadAssistantDialog
show={showLoadDialog}
dialogProps={loadDialogProps}
onCancel={() => setShowLoadDialog(false)}
onAssistantSelected={onAssistantSelected}
></LoadAssistantDialog>
<AssistantDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={() => setShowDialog(false)}
onConfirm={onConfirm}
></AssistantDialog>
</>
)
}
export default Assistants

View File

@ -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 */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')