Merge pull request #1252 from FlowiseAI/feature/OpenAI-Assistant
Feature/Add file annotations, sync and delete assistant
This commit is contained in:
commit
10c3066a91
|
|
@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode {
|
|||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
options.logger.info(`Clearing OpenAI Thread ${sessionId}`)
|
||||
await openai.beta.threads.del(sessionId)
|
||||
if (sessionId) await openai.beta.threads.del(sessionId)
|
||||
options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`)
|
||||
}
|
||||
|
||||
|
|
@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode {
|
|||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
|
||||
// Retrieve assistant
|
||||
try {
|
||||
const assistantDetails = JSON.parse(assistant.details)
|
||||
const openAIAssistantId = assistantDetails.id
|
||||
|
||||
// Retrieve assistant
|
||||
const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
|
||||
|
||||
if (formattedTools.length) {
|
||||
let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual)
|
||||
let filteredTools = []
|
||||
for (const tool of retrievedAssistant.tools) {
|
||||
if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool)
|
||||
}
|
||||
filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual)
|
||||
// filter out tool with empty function
|
||||
filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
|
||||
await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools })
|
||||
} else {
|
||||
let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function')
|
||||
await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools })
|
||||
}
|
||||
|
||||
const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
|
||||
|
|
@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode {
|
|||
})
|
||||
|
||||
let threadId = ''
|
||||
let isNewThread = false
|
||||
if (!chatmessage) {
|
||||
const thread = await openai.beta.threads.create({})
|
||||
threadId = thread.id
|
||||
isNewThread = true
|
||||
} else {
|
||||
const thread = await openai.beta.threads.retrieve(chatmessage.sessionId)
|
||||
threadId = thread.id
|
||||
}
|
||||
|
||||
// List all runs
|
||||
if (!isNewThread) {
|
||||
const promise = (threadId: string) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timeout = setInterval(async () => {
|
||||
const allRuns = await openai.beta.threads.runs.list(threadId)
|
||||
if (allRuns.data && allRuns.data.length) {
|
||||
const firstRunId = allRuns.data[0].id
|
||||
const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status
|
||||
if (
|
||||
runStatus &&
|
||||
(runStatus === 'cancelled' ||
|
||||
runStatus === 'completed' ||
|
||||
runStatus === 'expired' ||
|
||||
runStatus === 'failed')
|
||||
) {
|
||||
clearInterval(timeout)
|
||||
resolve()
|
||||
}
|
||||
} else {
|
||||
clearInterval(timeout)
|
||||
resolve()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
await promise(threadId)
|
||||
}
|
||||
|
||||
// Add message to thread
|
||||
await openai.beta.threads.messages.create(threadId, {
|
||||
role: 'user',
|
||||
|
|
@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode {
|
|||
})
|
||||
resolve(state)
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty`
|
||||
)
|
||||
)
|
||||
await openai.beta.threads.runs.cancel(threadId, runId)
|
||||
resolve('requires_action_retry')
|
||||
}
|
||||
}
|
||||
} else if (state === 'cancelled' || state === 'expired' || state === 'failed') {
|
||||
clearInterval(timeout)
|
||||
reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`))
|
||||
reject(
|
||||
new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`)
|
||||
)
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
// Polling run status
|
||||
let runThreadId = runThread.id
|
||||
let state = await promise(threadId, runThread.id)
|
||||
while (state === 'requires_action') {
|
||||
state = await promise(threadId, runThread.id)
|
||||
}
|
||||
|
||||
let retries = 3
|
||||
while (state === 'requires_action_retry') {
|
||||
if (retries > 0) {
|
||||
retries -= 1
|
||||
const newRunThread = await openai.beta.threads.runs.create(threadId, {
|
||||
assistant_id: retrievedAssistant.id
|
||||
})
|
||||
runThreadId = newRunThread.id
|
||||
state = await promise(threadId, newRunThread.id)
|
||||
} else {
|
||||
throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// List messages
|
||||
const messages = await openai.beta.threads.messages.list(threadId)
|
||||
const messageData = messages.data ?? []
|
||||
|
|
@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode {
|
|||
if (!assistantMessages.length) return ''
|
||||
|
||||
let returnVal = ''
|
||||
const fileAnnotations = []
|
||||
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
|
||||
if (content.text.annotations) {
|
||||
const message_content = content.text
|
||||
const annotations = message_content.annotations
|
||||
|
||||
const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant')
|
||||
|
||||
// Iterate over the annotations and add footnotes
|
||||
for (let index = 0; index < annotations.length; index++) {
|
||||
const annotation = annotations[index]
|
||||
let filePath = ''
|
||||
|
||||
// Gather citations based on annotation attributes
|
||||
const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation)
|
||||
.file_citation
|
||||
if (file_citation) {
|
||||
const cited_file = await openai.files.retrieve(file_citation.file_id)
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename
|
||||
filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName)
|
||||
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
|
||||
fileAnnotations.push({
|
||||
filePath,
|
||||
fileName
|
||||
})
|
||||
} else {
|
||||
const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path
|
||||
if (file_path) {
|
||||
const cited_file = await openai.files.retrieve(file_path.file_id)
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename
|
||||
filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName)
|
||||
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
|
||||
fileAnnotations.push({
|
||||
filePath,
|
||||
fileName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the text with a footnote
|
||||
message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`)
|
||||
}
|
||||
|
||||
returnVal += message_content.value
|
||||
} else {
|
||||
returnVal += content.text.value
|
||||
}
|
||||
} else {
|
||||
const content = assistantMessages[0].content[i] as MessageContentImageFile
|
||||
const fileId = content.image_file.file_id
|
||||
|
|
@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode {
|
|||
return {
|
||||
text: returnVal,
|
||||
usedTools,
|
||||
assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData }
|
||||
fileAnnotations,
|
||||
assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData }
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface IChatMessage {
|
|||
chatflowid: string
|
||||
sourceDocuments?: string
|
||||
usedTools?: string
|
||||
fileAnnotations?: string
|
||||
chatType: string
|
||||
chatId: string
|
||||
memoryType?: string
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage {
|
|||
@Column({ nullable: true, type: 'text' })
|
||||
usedTools?: string
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
fileAnnotations?: string
|
||||
|
||||
@Column()
|
||||
chatType: string
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations')
|
||||
if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
|||
import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory'
|
||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||
|
||||
export const mysqlMigrations = [
|
||||
Init1693840429259,
|
||||
|
|
@ -19,5 +20,6 @@ export const mysqlMigrations = [
|
|||
AddAnalytic1694432361423,
|
||||
AddChatHistory1694658767766,
|
||||
AddAssistantEntity1699325775451,
|
||||
AddUsedToolsToChatMessage1699481607341
|
||||
AddUsedToolsToChatMessage1699481607341,
|
||||
AddFileAnnotationsToChatMessage1700271021237
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac
|
|||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`)
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
|||
import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory'
|
||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||
|
||||
export const postgresMigrations = [
|
||||
Init1693891895163,
|
||||
|
|
@ -19,5 +20,6 @@ export const postgresMigrations = [
|
|||
AddAnalytic1694432361423,
|
||||
AddChatHistory1694658756136,
|
||||
AddAssistantEntity1699325775451,
|
||||
AddUsedToolsToChatMessage1699481607341
|
||||
AddUsedToolsToChatMessage1699481607341,
|
||||
AddFileAnnotationsToChatMessage1700271021237
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "chat_message";`)
|
||||
await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`)
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
|||
import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory'
|
||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||
|
||||
export const sqliteMigrations = [
|
||||
Init1693835579790,
|
||||
|
|
@ -19,5 +20,6 @@ export const sqliteMigrations = [
|
|||
AddAnalytic1694432361423,
|
||||
AddChatHistory1694657778173,
|
||||
AddAssistantEntity1699325775451,
|
||||
AddUsedToolsToChatMessage1699481607341
|
||||
AddUsedToolsToChatMessage1699481607341,
|
||||
AddFileAnnotationsToChatMessage1700271021237
|
||||
]
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ export class App {
|
|||
'/api/v1/node-icon/',
|
||||
'/api/v1/components-credentials-icon/',
|
||||
'/api/v1/chatflows-streaming',
|
||||
'/api/v1/openai-assistants-file',
|
||||
'/api/v1/ip'
|
||||
]
|
||||
this.app.use((req, res, next) => {
|
||||
|
|
@ -782,8 +783,8 @@ export class App {
|
|||
|
||||
await openai.beta.assistants.update(assistantDetails.id, {
|
||||
name: assistantDetails.name,
|
||||
description: assistantDetails.description,
|
||||
instructions: assistantDetails.instructions,
|
||||
description: assistantDetails.description ?? '',
|
||||
instructions: assistantDetails.instructions ?? '',
|
||||
model: assistantDetails.model,
|
||||
tools: filteredTools,
|
||||
file_ids: uniqWith(
|
||||
|
|
@ -952,7 +953,7 @@ export class App {
|
|||
|
||||
const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id })
|
||||
|
||||
await openai.beta.assistants.del(assistantDetails.id)
|
||||
if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id)
|
||||
|
||||
return res.json(results)
|
||||
} catch (error: any) {
|
||||
|
|
@ -961,6 +962,14 @@ export class App {
|
|||
}
|
||||
})
|
||||
|
||||
// Download file from assistant
|
||||
this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => {
|
||||
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath))
|
||||
const fileStream = fs.createReadStream(filePath)
|
||||
fileStream.pipe(res)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// Configuration
|
||||
// ----------------------------------------
|
||||
|
|
@ -1499,6 +1508,7 @@ export class App {
|
|||
}
|
||||
if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
|
||||
if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
|
||||
if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations)
|
||||
await this.addChatMessage(apiMessage)
|
||||
|
||||
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body)
|
|||
|
||||
const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body)
|
||||
|
||||
const deleteAssistant = (id) => client.delete(`/assistants/${id}`)
|
||||
const deleteAssistant = (id, isDeleteBoth) =>
|
||||
isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`)
|
||||
|
||||
export default {
|
||||
getAllAssistants,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax'
|
|||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import axios from 'axios'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
|
|
@ -28,7 +29,7 @@ import DatePicker from 'react-datepicker'
|
|||
import robotPNG from 'assets/images/robot.png'
|
||||
import userPNG from 'assets/images/account.png'
|
||||
import msgEmptySVG from 'assets/images/message_empty.svg'
|
||||
import { IconFileExport, IconEraser, IconX } from '@tabler/icons'
|
||||
import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons'
|
||||
|
||||
// Project import
|
||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||
|
|
@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm'
|
|||
// Utils
|
||||
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
|
||||
|
|
@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||
}
|
||||
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
||||
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
||||
obj[chatPK] = {
|
||||
|
|
@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||
}
|
||||
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||
if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools)
|
||||
if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||
|
||||
loadedMessages.push(obj)
|
||||
}
|
||||
|
|
@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||
window.open(data, '_blank')
|
||||
}
|
||||
|
||||
const downloadFile = async (fileAnnotation) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseURL}/api/v1/openai-assistants-file`,
|
||||
{ fileName: fileAnnotation.fileName },
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = fileAnnotation.fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onSourceDialogClick = (data, title) => {
|
||||
setSourceDialogProps({ data, title })
|
||||
setSourceDialogOpen(true)
|
||||
|
|
@ -648,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
{message.fileAnnotations && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||
return (
|
||||
<Button
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
textTransform: 'none',
|
||||
mb: 1,
|
||||
mr: 1
|
||||
}}
|
||||
key={index}
|
||||
variant='outlined'
|
||||
onClick={() => downloadFile(fileAnnotation)}
|
||||
endIcon={
|
||||
<IconDownload color={theme.palette.primary.main} />
|
||||
}
|
||||
>
|
||||
{fileAnnotation.fileName}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.sourceDocuments && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{removeDuplicateURL(message).map((source, index) => {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte
|
|||
|
||||
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'
|
||||
import { File } from 'ui-component/file/File'
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
import DeleteConfirmDialog from './DeleteConfirmDialog'
|
||||
|
||||
// Icons
|
||||
import { IconX } from '@tabler/icons'
|
||||
|
|
@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons'
|
|||
import assistantsApi from 'api/assistants'
|
||||
|
||||
// Hooks
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// utils
|
||||
|
|
@ -71,14 +70,8 @@ const assistantAvailableModels = [
|
|||
|
||||
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
|
|
@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
const [assistantFiles, setAssistantFiles] = useState([])
|
||||
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [deleteDialogProps, setDeleteDialogProps] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
|
|
@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (getAssistantObjApi.data) {
|
||||
setOpenAIAssistantId(getAssistantObjApi.data.id)
|
||||
setAssistantName(getAssistantObjApi.data.name)
|
||||
setAssistantDesc(getAssistantObjApi.data.description)
|
||||
setAssistantModel(getAssistantObjApi.data.model)
|
||||
setAssistantInstructions(getAssistantObjApi.data.instructions)
|
||||
setAssistantFiles(getAssistantObjApi.data.files ?? [])
|
||||
|
||||
let tools = []
|
||||
if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) {
|
||||
for (const tool of getAssistantObjApi.data.tools) {
|
||||
tools.push(tool.type)
|
||||
}
|
||||
}
|
||||
setAssistantTools(tools)
|
||||
syncData(getAssistantObjApi.data)
|
||||
}
|
||||
}, [getAssistantObjApi.data])
|
||||
|
||||
|
|
@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
const syncData = (data) => {
|
||||
setOpenAIAssistantId(data.id)
|
||||
setAssistantName(data.name)
|
||||
setAssistantDesc(data.description)
|
||||
setAssistantModel(data.model)
|
||||
setAssistantInstructions(data.instructions)
|
||||
setAssistantFiles(data.files ?? [])
|
||||
|
||||
let tools = []
|
||||
if (data.tools && data.tools.length) {
|
||||
for (const tool of data.tools) {
|
||||
tools.push(tool.type)
|
||||
}
|
||||
}
|
||||
setAssistantTools(tools)
|
||||
}
|
||||
|
||||
const addNewAssistant = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
|
|
@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
}
|
||||
}
|
||||
|
||||
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}`
|
||||
const onSyncClick = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential)
|
||||
if (getResp.data) {
|
||||
syncData(getResp.data)
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete Assistant: ${errorData}`,
|
||||
message: 'Assistant successfully synced!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
|
|
@ -351,8 +326,71 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to sync Assistant: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteClick = () => {
|
||||
setDeleteDialogProps({
|
||||
title: `Delete Assistant`,
|
||||
description: `Delete Assistant ${assistantName}?`,
|
||||
cancelButtonName: 'Cancel'
|
||||
})
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const deleteAssistant = async (isDeleteBoth) => {
|
||||
setDeleteDialogOpen(false)
|
||||
try {
|
||||
const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
</DialogContent>
|
||||
<DialogActions>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => deleteAssistant()}>
|
||||
<StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}>
|
||||
Sync
|
||||
</StyledButton>
|
||||
)}
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => onDeleteClick()}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
)}
|
||||
|
|
@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
|||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
<DeleteConfirmDialog
|
||||
show={deleteDialogOpen}
|
||||
dialogProps={deleteDialogProps}
|
||||
onCancel={() => setDeleteDialogOpen(false)}
|
||||
onDelete={() => deleteAssistant()}
|
||||
onDeleteBoth={() => deleteAssistant(true)}
|
||||
/>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</Dialog>
|
||||
) : null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
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>
|
||||
<span>{dialogProps.description}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 20 }}>
|
||||
<StyledButton sx={{ mb: 1 }} color='orange' variant='contained' onClick={onDelete}>
|
||||
Delete only from Flowise
|
||||
</StyledButton>
|
||||
<StyledButton sx={{ mb: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
|
||||
Delete from both OpenAI and Flowise
|
||||
</StyledButton>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
DeleteConfirmDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onDeleteBoth: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default DeleteConfirmDialog
|
||||
|
|
@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax'
|
|||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import axios from 'axios'
|
||||
|
||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material'
|
||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconSend } from '@tabler/icons'
|
||||
import { IconSend, IconDownload } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||
|
|
@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
|||
|
||||
setMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
{ message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' }
|
||||
{
|
||||
message: text,
|
||||
sourceDocuments: data?.sourceDocuments,
|
||||
usedTools: data?.usedTools,
|
||||
fileAnnotations: data?.fileAnnotations,
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (fileAnnotation) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseURL}/api/v1/openai-assistants-file`,
|
||||
{ fileName: fileAnnotation.fileName },
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = fileAnnotation.fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get chatmessages successful
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data?.length) {
|
||||
|
|
@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
|||
}
|
||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
|
||||
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
|
||||
return obj
|
||||
})
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
|
|
@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
|||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
{message.fileAnnotations && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||
return (
|
||||
<Button
|
||||
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
|
||||
key={index}
|
||||
variant='outlined'
|
||||
onClick={() => downloadFile(fileAnnotation)}
|
||||
endIcon={<IconDownload color={theme.palette.primary.main} />}
|
||||
>
|
||||
{fileAnnotation.fileName}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.sourceDocuments && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{removeDuplicateURL(message).map((source, index) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue