Feature/OpenAI Assistant V2 (#2258)

* add gpt4 turbo to assistant

* OpenAI Assistant V2

* update langfuse handler
This commit is contained in:
Henry Heng
2024-04-25 20:14:04 +01:00
committed by GitHub
parent 4782c0f6fc
commit 7360d1d9a6
25 changed files with 23422 additions and 17637 deletions
+2 -1
View File
@@ -64,6 +64,7 @@
},
"resolutions": {
"@qdrant/openapi-typescript-fetch": "1.2.1",
"@google/generative-ai": "^0.7.0"
"@google/generative-ai": "^0.7.0",
"openai": "4.38.3"
}
}
@@ -1,16 +1,17 @@
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams, IUsedTool } from '../../../src/Interface'
import OpenAI from 'openai'
import { DataSource } from 'typeorm'
import { getCredentialData, getCredentialParam, getUserHome } from '../../../src/utils'
import { ImageFileContentBlock, TextContentBlock } from 'openai/resources/beta/threads/messages/messages'
import * as fsDefault from 'node:fs'
import * as path from 'node:path'
import { getCredentialData, getCredentialParam } from '../../../src/utils'
import fetch from 'node-fetch'
import { flatten, uniqWith, isEqual } from 'lodash'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { AnalyticHandler } from '../../../src/handler'
import { Moderation, checkInputs, streamResponse } from '../../moderation/Moderation'
import { formatResponse } from '../../outputparsers/OutputParserHelpers'
import { addFileToStorage } from '../../../src/storageUtils'
const lenticularBracketRegex = /【[^】]*】/g
const imageRegex = /<img[^>]*\/>/g
class OpenAIAssistant_Agents implements INode {
label: string
@@ -168,6 +169,9 @@ class OpenAIAssistant_Agents implements INode {
tools = flatten(tools)
const formattedTools = tools?.map((tool: any) => formatToOpenAIAssistantTool(tool)) ?? []
const usedTools: IUsedTool[] = []
const fileAnnotations = []
const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({
id: selectedAssistantId
})
@@ -195,7 +199,7 @@ class OpenAIAssistant_Agents implements INode {
if (formattedTools.length) {
let filteredTools = []
for (const tool of retrievedAssistant.tools) {
if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool)
if (tool.type === 'code_interpreter' || tool.type === 'file_search') filteredTools.push(tool)
}
filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual)
// filter out tool with empty function
@@ -236,7 +240,8 @@ class OpenAIAssistant_Agents implements INode {
(runStatus === 'cancelled' ||
runStatus === 'completed' ||
runStatus === 'expired' ||
runStatus === 'failed')
runStatus === 'failed' ||
runStatus === 'requires_action')
) {
clearInterval(timeout)
resolve()
@@ -259,11 +264,235 @@ class OpenAIAssistant_Agents implements INode {
// Run assistant thread
const llmIds = await analyticHandlers.onLLMStart('ChatOpenAI', input, parentIds)
const runThread = await openai.beta.threads.runs.create(threadId, {
assistant_id: retrievedAssistant.id
})
const usedTools: IUsedTool[] = []
let text = ''
let runThreadId = ''
let isStreamingStarted = false
if (isStreaming) {
const streamThread = await openai.beta.threads.runs.create(threadId, {
assistant_id: retrievedAssistant.id,
stream: true
})
for await (const event of streamThread) {
if (event.event === 'thread.run.created') {
runThreadId = event.data.id
}
if (event.event === 'thread.message.delta') {
const chunk = event.data.delta.content?.[0]
if (chunk && 'text' in chunk) {
if (chunk.text?.annotations?.length) {
const message_content = chunk.text
const annotations = chunk.text?.annotations
// Iterate over the annotations
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.FileCitationAnnotation).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
if (!disableFileDownload) {
filePath = await downloadFile(openAIApiKey, cited_file, fileName, options.chatflowid, threadId)
fileAnnotations.push({
filePath,
fileName
})
}
} else {
const file_path = (annotation as OpenAI.Beta.Threads.Messages.FilePathAnnotation).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
if (!disableFileDownload) {
filePath = await downloadFile(
openAIApiKey,
cited_file,
fileName,
options.chatflowid,
threadId
)
fileAnnotations.push({
filePath,
fileName
})
}
}
}
// Replace the text with a footnote
message_content.value = message_content.value?.replace(
`${annotation.text}`,
`${disableFileDownload ? '' : filePath}`
)
}
// Remove lenticular brackets
message_content.value = message_content.value?.replace(lenticularBracketRegex, '')
text += message_content.value ?? ''
if (message_content.value) {
if (!isStreamingStarted) {
isStreamingStarted = true
socketIO.to(socketIOClientId).emit('start', message_content.value)
}
socketIO.to(socketIOClientId).emit('token', message_content.value)
}
if (fileAnnotations.length) {
if (!isStreamingStarted) {
isStreamingStarted = true
socketIO.to(socketIOClientId).emit('start', '')
}
socketIO.to(socketIOClientId).emit('fileAnnotations', fileAnnotations)
}
} else {
text += chunk.text?.value
if (!isStreamingStarted) {
isStreamingStarted = true
socketIO.to(socketIOClientId).emit('start', chunk.text?.value)
}
socketIO.to(socketIOClientId).emit('token', chunk.text?.value)
}
}
if (chunk && 'image_file' in chunk && chunk.image_file?.file_id) {
const fileId = chunk.image_file.file_id
const fileObj = await openai.files.retrieve(fileId)
const buffer = await downloadImg(openai, fileId, `${fileObj.filename}.png`, options.chatflowid, threadId)
const base64String = Buffer.from(buffer).toString('base64')
// TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits
const imgHTML = `<img src="data:image/png;base64,${base64String}" width="100%" height="max-content" alt="${fileObj.filename}" /><br/>`
text += imgHTML
if (!isStreamingStarted) {
isStreamingStarted = true
socketIO.to(socketIOClientId).emit('start', imgHTML)
}
socketIO.to(socketIOClientId).emit('token', imgHTML)
}
}
if (event.event === 'thread.run.requires_action') {
if (event.data.required_action?.submit_tool_outputs.tool_calls) {
const actions: ICommonObject[] = []
event.data.required_action.submit_tool_outputs.tool_calls.forEach((item) => {
const functionCall = item.function
let args = {}
try {
args = JSON.parse(functionCall.arguments)
} catch (e) {
console.error('Error parsing arguments, default to empty object')
}
actions.push({
tool: functionCall.name,
toolInput: args,
toolCallId: item.id
})
})
const submitToolOutputs = []
for (let i = 0; i < actions.length; i += 1) {
const tool = tools.find((tool: any) => tool.name === actions[i].tool)
if (!tool) continue
// Start tool analytics
const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds)
try {
const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, {
sessionId: threadId,
chatId: options.chatId,
input
})
await analyticHandlers.onToolEnd(toolIds, toolOutput)
submitToolOutputs.push({
tool_call_id: actions[i].toolCallId,
output: toolOutput
})
usedTools.push({
tool: tool.name,
toolInput: actions[i].toolInput,
toolOutput
})
} catch (e) {
await analyticHandlers.onToolEnd(toolIds, e)
console.error('Error executing tool', e)
throw new Error(
`Error executing tool. Tool: ${tool.name}. Thread ID: ${threadId}. Run ID: ${runThreadId}`
)
}
}
try {
const stream = openai.beta.threads.runs.submitToolOutputsStream(threadId, runThreadId, {
tool_outputs: submitToolOutputs
})
for await (const event of stream) {
if (event.event === 'thread.message.delta') {
const chunk = event.data.delta.content?.[0]
if (chunk && 'text' in chunk && chunk.text?.value) {
text += chunk.text.value
if (!isStreamingStarted) {
isStreamingStarted = true
socketIO.to(socketIOClientId).emit('start', chunk.text.value)
}
socketIO.to(socketIOClientId).emit('token', chunk.text.value)
}
}
}
socketIO.to(socketIOClientId).emit('usedTools', usedTools)
} catch (error) {
console.error('Error submitting tool outputs:', error)
await openai.beta.threads.runs.cancel(threadId, runThreadId)
const errMsg = `Error submitting tool outputs. Thread ID: ${threadId}. Run ID: ${runThreadId}`
await analyticHandlers.onLLMError(llmIds, errMsg)
await analyticHandlers.onChainError(parentIds, errMsg, true)
throw new Error(errMsg)
}
}
}
}
// 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 ''
// Remove images from the logging text
let llmOutput = text.replace(imageRegex, '')
llmOutput = llmOutput.replace('<br/>', '')
await analyticHandlers.onLLMEnd(llmIds, llmOutput)
await analyticHandlers.onChainEnd(parentIds, messageData, true)
return {
text,
usedTools,
fileAnnotations,
assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData }
}
}
const promise = (threadId: string, runId: string) => {
return new Promise((resolve, reject) => {
@@ -299,8 +528,7 @@ class OpenAIAssistant_Agents implements INode {
// Start tool analytics
const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds)
if (options.socketIO && options.socketIOClientId)
options.socketIO.to(options.socketIOClientId).emit('tool', tool.name)
if (socketIO && socketIOClientId) socketIO.to(socketIOClientId).emit('tool', tool.name)
try {
const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, {
@@ -360,7 +588,10 @@ class OpenAIAssistant_Agents implements INode {
}
// Polling run status
let runThreadId = runThread.id
const runThread = await openai.beta.threads.runs.create(threadId, {
assistant_id: retrievedAssistant.id
})
runThreadId = runThread.id
let state = await promise(threadId, runThread.id)
while (state === 'requires_action') {
state = await promise(threadId, runThread.id)
@@ -389,17 +620,14 @@ 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 TextContentBlock
const content = assistantMessages[0].content[i] as OpenAI.Beta.Threads.Messages.TextContentBlock
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
for (let index = 0; index < annotations.length; index++) {
const annotation = annotations[index]
@@ -407,13 +635,13 @@ class OpenAIAssistant_Agents implements INode {
// Gather citations based on annotation attributes
const file_citation = (annotation as OpenAI.Beta.Threads.Messages.FileCitationAnnotation).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)
if (!disableFileDownload) {
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
filePath = await downloadFile(openAIApiKey, cited_file, fileName, options.chatflowid, threadId)
fileAnnotations.push({
filePath,
fileName
@@ -425,9 +653,8 @@ class OpenAIAssistant_Agents implements INode {
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)
if (!disableFileDownload) {
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
filePath = await downloadFile(openAIApiKey, cited_file, fileName, options.chatflowid, threadId)
fileAnnotations.push({
filePath,
fileName
@@ -448,19 +675,14 @@ class OpenAIAssistant_Agents implements INode {
returnVal += content.text.value
}
const lenticularBracketRegex = /【[^】]*】/g
returnVal = returnVal.replace(lenticularBracketRegex, '')
} else {
const content = assistantMessages[0].content[i] as ImageFileContentBlock
const content = assistantMessages[0].content[i] as OpenAI.Beta.Threads.Messages.ImageFileContentBlock
const fileId = content.image_file.file_id
const fileObj = await openai.files.retrieve(fileId)
const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant')
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', `${fileObj.filename}.png`)
await downloadImg(openai, fileId, filePath, dirPath)
const bitmap = fsDefault.readFileSync(filePath)
const base64String = Buffer.from(bitmap).toString('base64')
const buffer = await downloadImg(openai, fileId, `${fileObj.filename}.png`, options.chatflowid, threadId)
const base64String = Buffer.from(buffer).toString('base64')
// TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits
const imgHTML = `<img src="data:image/png;base64,${base64String}" width="100%" height="max-content" alt="${fileObj.filename}" /><br/>`
@@ -468,7 +690,6 @@ class OpenAIAssistant_Agents implements INode {
}
}
const imageRegex = /<img[^>]*\/>/g
let llmOutput = returnVal.replace(imageRegex, '')
llmOutput = llmOutput.replace('<br/>', '')
@@ -488,7 +709,7 @@ class OpenAIAssistant_Agents implements INode {
}
}
const downloadImg = async (openai: OpenAI, fileId: string, filePath: string, dirPath: string) => {
const downloadImg = async (openai: OpenAI, fileId: string, fileName: string, ...paths: string[]) => {
const response = await openai.files.content(fileId)
// Extract the binary data from the Response object
@@ -496,15 +717,14 @@ const downloadImg = async (openai: OpenAI, fileId: string, filePath: string, dir
// Convert the binary data to a Buffer
const image_data_buffer = Buffer.from(image_data)
const mime = 'image/png'
// Save the image to a specific location
if (!fsDefault.existsSync(dirPath)) {
fsDefault.mkdirSync(path.dirname(filePath), { recursive: true })
}
fsDefault.writeFileSync(filePath, image_data_buffer)
await addFileToStorage(mime, image_data_buffer, fileName, ...paths)
return image_data_buffer
}
const downloadFile = async (fileObj: any, filePath: string, dirPath: string, openAIApiKey: string) => {
const downloadFile = async (openAIApiKey: string, fileObj: any, fileName: string, ...paths: string[]) => {
try {
const response = await fetch(`https://api.openai.com/v1/files/${fileObj.id}/content`, {
method: 'GET',
@@ -515,20 +735,17 @@ const downloadFile = async (fileObj: any, filePath: string, dirPath: string, ope
throw new Error(`HTTP error! status: ${response.status}`)
}
await new Promise<void>((resolve, reject) => {
if (!fsDefault.existsSync(dirPath)) {
fsDefault.mkdirSync(path.dirname(filePath), { recursive: true })
}
const dest = fsDefault.createWriteStream(filePath)
response.body.pipe(dest)
response.body.on('end', () => resolve())
dest.on('error', reject)
})
// Extract the binary data from the Response object
const data = await response.arrayBuffer()
// eslint-disable-next-line no-console
console.log('File downloaded and written to', filePath)
// Convert the binary data to a Buffer
const data_buffer = Buffer.from(data)
const mime = 'application/octet-stream'
return await addFileToStorage(mime, data_buffer, fileName, ...paths)
} catch (error) {
console.error('Error downloading or writing the file:', error)
return ''
}
}
+1 -1
View File
@@ -91,7 +91,7 @@
"node-html-markdown": "^1.3.0",
"notion-to-md": "^3.1.1",
"object-hash": "^3.0.0",
"openai": "^4.32.1",
"openai": "^4.38.3",
"pdf-parse": "^1.1.1",
"pdfjs-dist": "^3.7.107",
"pg": "^8.11.2",
+21
View File
@@ -420,6 +420,11 @@ export class AnalyticHandler {
}
if (langfuseTraceClient) {
langfuseTraceClient.update({
input: {
text: input
}
})
const span = langfuseTraceClient.span({
name,
input: {
@@ -472,6 +477,14 @@ export class AnalyticHandler {
span.end({
output
})
const langfuseTraceClient = this.handlers['langFuse'].trace[returnIds['langFuse'].trace]
if (langfuseTraceClient) {
langfuseTraceClient.update({
output: {
output
}
})
}
if (shutdown) {
const langfuse: Langfuse = this.handlers['langFuse'].client
await langfuse.shutdownAsync()
@@ -513,6 +526,14 @@ export class AnalyticHandler {
error
}
})
const langfuseTraceClient = this.handlers['langFuse'].trace[returnIds['langFuse'].trace]
if (langfuseTraceClient) {
langfuseTraceClient.update({
output: {
error
}
})
}
if (shutdown) {
const langfuse: Langfuse = this.handlers['langFuse'].client
await langfuse.shutdownAsync()
+4 -2
View File
@@ -14,10 +14,10 @@ export const addBase64FilesToStorage = async (file: string, chatflowid: string,
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const mime = splitDataURI[0].split(':')[1].split(';')[0]
const key = chatflowid + '/' + filename
const Key = chatflowid + '/' + filename
const putObjCmd = new PutObjectCommand({
Bucket,
Key: key,
Key,
ContentEncoding: 'base64', // required for binary data
ContentType: mime,
Body: bf
@@ -61,6 +61,7 @@ export const addFileToStorage = async (mime: string, bf: Buffer, fileName: strin
Body: bf
})
await s3Client.send(putObjCmd)
return 'FILE-STORAGE::' + fileName
} else {
const dir = path.join(getStoragePath(), ...paths)
if (!fs.existsSync(dir)) {
@@ -69,6 +70,7 @@ export const addFileToStorage = async (mime: string, bf: Buffer, fileName: strin
const filePath = path.join(dir, fileName)
fs.writeFileSync(filePath, bf)
return 'FILE-STORAGE::' + fileName
}
}
@@ -0,0 +1,201 @@
import { Request, Response, NextFunction } from 'express'
import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import openAIAssistantVectorStoreService from '../../services/openai-assistants-vector-store'
const getAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.getAssistantVectorStore - id not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.getAssistantVectorStore - credential not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.getAssistantVectorStore(req.query.credential as string, req.params.id)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const listAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.listAssistantVectorStore - credential not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.listAssistantVectorStore(req.query.credential as string)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const createAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.body) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.createAssistantVectorStore - body not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.createAssistantVectorStore - credential not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.createAssistantVectorStore(req.query.credential as string, req.body)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const updateAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.updateAssistantVectorStore - id not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.updateAssistantVectorStore - credential not provided!`
)
}
if (!req.body) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.updateAssistantVectorStore - body not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.updateAssistantVectorStore(
req.query.credential as string,
req.params.id,
req.body
)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const deleteAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.deleteAssistantVectorStore - id not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.updateAssistantVectorStore - credential not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.deleteAssistantVectorStore(
req.query.credential as string,
req.params.id as string
)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const uploadFilesToAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.body) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.uploadFilesToAssistantVectorStore - body not provided!`
)
}
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.uploadFilesToAssistantVectorStore - id not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.uploadFilesToAssistantVectorStore - credential not provided!`
)
}
const files = req.files ?? []
const uploadFiles: { filePath: string; fileName: string }[] = []
if (Array.isArray(files)) {
for (const file of files) {
uploadFiles.push({
filePath: file.path,
fileName: file.originalname
})
}
}
const apiResponse = await openAIAssistantVectorStoreService.uploadFilesToAssistantVectorStore(
req.query.credential as string,
req.params.id as string,
uploadFiles
)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const deleteFilesFromAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.body) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.deleteFilesFromAssistantVectorStore - body not provided!`
)
}
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.deleteFilesFromAssistantVectorStore - id not provided!`
)
}
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.deleteFilesFromAssistantVectorStore - credential not provided!`
)
}
const apiResponse = await openAIAssistantVectorStoreService.deleteFilesFromAssistantVectorStore(
req.query.credential as string,
req.params.id as string,
req.body.file_ids
)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
getAssistantVectorStore,
listAssistantVectorStore,
createAssistantVectorStore,
updateAssistantVectorStore,
deleteAssistantVectorStore,
uploadFilesToAssistantVectorStore,
deleteFilesFromAssistantVectorStore
}
@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from 'express'
import path from 'path'
import * as fs from 'fs'
import openaiAssistantsService from '../../services/openai-assistants'
import { getUserHome } from '../../utils'
import contentDisposition from 'content-disposition'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { streamStorageFile } from 'flowise-components'
// List available assistants
const getAllOpenaiAssistants = async (req: Request, res: Response, next: NextFunction) => {
@@ -48,27 +47,57 @@ const getSingleOpenaiAssistant = async (req: Request, res: Response, next: NextF
// Download file from assistant
const getFileFromAssistant = async (req: Request, res: Response, next: NextFunction) => {
try {
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
//raise error if file path is not absolute
if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
//raise error if file path contains '..'
if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
//only return from the .flowise openai-assistant folder
if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`)
if (fs.existsSync(filePath)) {
res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath)))
const fileStream = fs.createReadStream(filePath)
if (!req.body.chatflowId || !req.body.chatId || !req.body.fileName) {
return res.status(500).send(`Invalid file path`)
}
const chatflowId = req.body.chatflowId as string
const chatId = req.body.chatId as string
const fileName = req.body.fileName as string
res.setHeader('Content-Disposition', contentDisposition(fileName))
const fileStream = await streamStorageFile(chatflowId, chatId, fileName)
if (!fileStream) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: getFileFromAssistant`)
if (fileStream instanceof fs.ReadStream && fileStream?.pipe) {
fileStream.pipe(res)
} else {
return res.status(404).send(`File ${req.body.fileName} not found`)
res.send(fileStream)
}
} catch (error) {
next(error)
}
}
const uploadAssistantFiles = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.query === 'undefined' || !req.query.credential) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: openaiAssistantsVectorStoreController.uploadFilesToAssistantVectorStore - credential not provided!`
)
}
const files = req.files ?? []
const uploadFiles: { filePath: string; fileName: string }[] = []
if (Array.isArray(files)) {
for (const file of files) {
uploadFiles.push({
filePath: file.path,
fileName: file.originalname
})
}
}
const apiResponse = await openaiAssistantsService.uploadFilesToAssistant(req.query.credential as string, uploadFiles)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
getAllOpenaiAssistants,
getSingleOpenaiAssistant,
getFileFromAssistant
getFileFromAssistant,
uploadAssistantFiles
}
+1 -1
View File
@@ -135,7 +135,7 @@ export class App {
'/api/v1/components-credentials-icon/',
'/api/v1/chatflows-streaming',
'/api/v1/chatflows-uploads',
'/api/v1/openai-assistants-file',
'/api/v1/openai-assistants-file/download',
'/api/v1/feedback',
'/api/v1/get-upload-file',
'/api/v1/ip'
+2
View File
@@ -25,6 +25,7 @@ import nodeLoadMethodRouter from './node-load-methods'
import nodesRouter from './nodes'
import openaiAssistantsRouter from './openai-assistants'
import openaiAssistantsFileRouter from './openai-assistants-files'
import openaiAssistantsVectorStoreRouter from './openai-assistants-vector-store'
import predictionRouter from './predictions'
import promptListsRouter from './prompts-lists'
import publicChatbotRouter from './public-chatbots'
@@ -65,6 +66,7 @@ router.use('/node-load-method', nodeLoadMethodRouter)
router.use('/nodes', nodesRouter)
router.use('/openai-assistants', openaiAssistantsRouter)
router.use('/openai-assistants-file', openaiAssistantsFileRouter)
router.use('/openai-assistants-vector-store', openaiAssistantsVectorStoreRouter)
router.use('/prediction', predictionRouter)
router.use('/prompts-list', promptListsRouter)
router.use('/public-chatbotConfig', publicChatbotRouter)
@@ -1,8 +1,12 @@
import express from 'express'
import multer from 'multer'
import path from 'path'
import openaiAssistantsController from '../../controllers/openai-assistants'
const router = express.Router()
// CREATE
router.post('/', openaiAssistantsController.getFileFromAssistant)
const router = express.Router()
const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` })
router.post('/download/', openaiAssistantsController.getFileFromAssistant)
router.post('/upload/', upload.array('files'), openaiAssistantsController.uploadAssistantFiles)
export default router
@@ -0,0 +1,30 @@
import express from 'express'
import multer from 'multer'
import path from 'path'
import openaiAssistantsVectorStoreController from '../../controllers/openai-assistants-vector-store'
const router = express.Router()
const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` })
// CREATE
router.post('/', openaiAssistantsVectorStoreController.createAssistantVectorStore)
// READ
router.get('/:id', openaiAssistantsVectorStoreController.getAssistantVectorStore)
// LIST
router.get('/', openaiAssistantsVectorStoreController.listAssistantVectorStore)
// UPDATE
router.put(['/', '/:id'], openaiAssistantsVectorStoreController.updateAssistantVectorStore)
// DELETE
router.delete(['/', '/:id'], openaiAssistantsVectorStoreController.deleteAssistantVectorStore)
// POST
router.post('/:id', upload.array('files'), openaiAssistantsVectorStoreController.uploadFilesToAssistantVectorStore)
// DELETE
router.patch(['/', '/:id'], openaiAssistantsVectorStoreController.deleteFilesFromAssistantVectorStore)
export default router
@@ -1,12 +1,10 @@
import OpenAI from 'openai'
import path from 'path'
import * as fs from 'fs'
import { StatusCodes } from 'http-status-codes'
import { uniqWith, isEqual } from 'lodash'
import { uniqWith, isEqual, cloneDeep } from 'lodash'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Assistant } from '../../database/entities/Assistant'
import { Credential } from '../../database/entities/Credential'
import { getUserHome, decryptCredentialData, getAppVersion } from '../../utils'
import { decryptCredentialData, getAppVersion } from '../../utils'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
@@ -34,6 +32,7 @@ const createAssistant = async (requestBody: any): Promise<any> => {
}
const openai = new OpenAI({ apiKey: openAIApiKey })
// Prepare tools
let tools = []
if (assistantDetails.tools) {
for (const tool of assistantDetails.tools ?? []) {
@@ -43,40 +42,25 @@ const createAssistant = async (requestBody: any): Promise<any> => {
}
}
if (assistantDetails.uploadFiles) {
// Base64 strings
let files: string[] = []
const fileBase64 = assistantDetails.uploadFiles
if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
files = JSON.parse(fileBase64)
} else {
files = [fileBase64]
}
// Save tool_resources to be stored later into database
const savedToolResources = cloneDeep(assistantDetails.tool_resources)
const uploadedFiles = []
for (const file of files) {
const splitDataURI = file.split(',')
const filename = splitDataURI.pop()?.split(':')[1] ?? ''
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
// Cleanup tool_resources for creating assistant
if (assistantDetails.tool_resources) {
for (const toolResource in assistantDetails.tool_resources) {
if (toolResource === 'file_search') {
assistantDetails.tool_resources['file_search'] = {
vector_store_ids: assistantDetails.tool_resources['file_search'].vector_store_ids
}
} else if (toolResource === 'code_interpreter') {
assistantDetails.tool_resources['code_interpreter'] = {
file_ids: assistantDetails.tool_resources['code_interpreter'].file_ids
}
}
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, bf)
}
const createdFile = await openai.files.create({
file: fs.createReadStream(filePath),
purpose: 'assistants'
})
uploadedFiles.push(createdFile)
fs.unlinkSync(filePath)
}
assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
}
// If the assistant doesn't exist, create a new one
if (!assistantDetails.id) {
const newAssistant = await openai.beta.assistants.create({
name: assistantDetails.name,
@@ -84,12 +68,15 @@ const createAssistant = async (requestBody: any): Promise<any> => {
instructions: assistantDetails.instructions,
model: assistantDetails.model,
tools,
file_ids: (assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)
tool_resources: assistantDetails.tool_resources,
temperature: assistantDetails.temperature,
top_p: assistantDetails.top_p
})
assistantDetails.id = newAssistant.id
} else {
const retrievedAssistant = await openai.beta.assistants.retrieve(assistantDetails.id)
let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
let filteredTools = uniqWith([...retrievedAssistant.tools.filter((tool) => tool.type === 'function'), ...tools], isEqual)
// Remove empty functions
filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
await openai.beta.assistants.update(assistantDetails.id, {
@@ -98,17 +85,16 @@ const createAssistant = async (requestBody: any): Promise<any> => {
instructions: assistantDetails.instructions ?? '',
model: assistantDetails.model,
tools: filteredTools,
file_ids: uniqWith(
[...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)],
isEqual
)
tool_resources: assistantDetails.tool_resources,
temperature: assistantDetails.temperature,
top_p: assistantDetails.top_p
})
}
const newAssistantDetails = {
...assistantDetails
}
if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
if (savedToolResources) newAssistantDetails.tool_resources = savedToolResources
requestBody.details = JSON.stringify(newAssistantDetails)
} catch (error) {
@@ -117,7 +103,7 @@ const createAssistant = async (requestBody: any): Promise<any> => {
const newAssistant = new Assistant()
Object.assign(newAssistant, requestBody)
const assistant = await appServer.AppDataSource.getRepository(Assistant).create(newAssistant)
const assistant = appServer.AppDataSource.getRepository(Assistant).create(newAssistant)
const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant)
await appServer.telemetry.sendTelemetry('assistant_created', {
@@ -249,42 +235,26 @@ const updateAssistant = async (assistantId: string, requestBody: any): Promise<a
}
}
if (assistantDetails.uploadFiles) {
// Base64 strings
let files: string[] = []
const fileBase64 = assistantDetails.uploadFiles
if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
files = JSON.parse(fileBase64)
} else {
files = [fileBase64]
}
// Save tool_resources to be stored later into database
const savedToolResources = cloneDeep(assistantDetails.tool_resources)
const uploadedFiles = []
for (const file of files) {
const splitDataURI = file.split(',')
const filename = splitDataURI.pop()?.split(':')[1] ?? ''
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
// Cleanup tool_resources before updating
if (assistantDetails.tool_resources) {
for (const toolResource in assistantDetails.tool_resources) {
if (toolResource === 'file_search') {
assistantDetails.tool_resources['file_search'] = {
vector_store_ids: assistantDetails.tool_resources['file_search'].vector_store_ids
}
} else if (toolResource === 'code_interpreter') {
assistantDetails.tool_resources['code_interpreter'] = {
file_ids: assistantDetails.tool_resources['code_interpreter'].file_ids
}
}
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, bf)
}
const createdFile = await openai.files.create({
file: fs.createReadStream(filePath),
purpose: 'assistants'
})
uploadedFiles.push(createdFile)
fs.unlinkSync(filePath)
}
assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
}
const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
let filteredTools = uniqWith([...retrievedAssistant.tools.filter((tool) => tool.type === 'function'), ...tools], isEqual)
filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
await openai.beta.assistants.update(openAIAssistantId, {
@@ -293,23 +263,22 @@ const updateAssistant = async (assistantId: string, requestBody: any): Promise<a
instructions: assistantDetails.instructions,
model: assistantDetails.model,
tools: filteredTools,
file_ids: uniqWith(
[...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)],
isEqual
)
tool_resources: assistantDetails.tool_resources,
temperature: assistantDetails.temperature,
top_p: assistantDetails.top_p
})
const newAssistantDetails = {
...assistantDetails,
id: openAIAssistantId
}
if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
if (savedToolResources) newAssistantDetails.tool_resources = savedToolResources
const updateAssistant = new Assistant()
body.details = JSON.stringify(newAssistantDetails)
Object.assign(updateAssistant, body)
await appServer.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant)
appServer.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant)
const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant)
return dbResponse
} catch (error) {
@@ -0,0 +1,257 @@
import OpenAI from 'openai'
import { StatusCodes } from 'http-status-codes'
import fs from 'fs'
import { Credential } from '../../database/entities/Credential'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { decryptCredentialData } from '../../utils'
const getAssistantVectorStore = async (credentialId: string, vectorStoreId: string) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const dbResponse = await openai.beta.vectorStores.retrieve(vectorStoreId)
return dbResponse
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.getAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const listAssistantVectorStore = async (credentialId: string) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const dbResponse = await openai.beta.vectorStores.list()
return dbResponse.data
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.listAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const createAssistantVectorStore = async (credentialId: string, obj: OpenAI.Beta.VectorStores.VectorStoreCreateParams) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const dbResponse = await openai.beta.vectorStores.create(obj)
return dbResponse
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.createAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const updateAssistantVectorStore = async (
credentialId: string,
vectorStoreId: string,
obj: OpenAI.Beta.VectorStores.VectorStoreUpdateParams
) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const dbResponse = await openai.beta.vectorStores.update(vectorStoreId, obj)
const vectorStoreFiles = await openai.beta.vectorStores.files.list(vectorStoreId)
if (vectorStoreFiles.data?.length) {
const files = []
for (const file of vectorStoreFiles.data) {
const fileData = await openai.files.retrieve(file.id)
files.push(fileData)
}
;(dbResponse as any).files = files
}
return dbResponse
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.updateAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const deleteAssistantVectorStore = async (credentialId: string, vectorStoreId: string) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const dbResponse = await openai.beta.vectorStores.del(vectorStoreId)
return dbResponse
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.deleteAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const uploadFilesToAssistantVectorStore = async (
credentialId: string,
vectorStoreId: string,
files: { filePath: string; fileName: string }[]
): Promise<any> => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const uploadedFiles = []
for (const file of files) {
const createdFile = await openai.files.create({
file: new File([new Blob([fs.readFileSync(file.filePath)])], file.fileName),
purpose: 'assistants'
})
uploadedFiles.push(createdFile)
fs.unlinkSync(file.filePath)
}
const file_ids = [...uploadedFiles.map((file) => file.id)]
const res = await openai.beta.vectorStores.fileBatches.createAndPoll(vectorStoreId, {
file_ids
})
if (res.status === 'completed' && res.file_counts.completed === uploadedFiles.length) return uploadedFiles
else if (res.status === 'failed')
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
'Error: openaiAssistantsVectorStoreService.uploadFilesToAssistantVectorStore - Upload failed!'
)
else
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
'Error: openaiAssistantsVectorStoreService.uploadFilesToAssistantVectorStore - Upload cancelled!'
)
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.uploadFilesToAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
const deleteFilesFromAssistantVectorStore = async (credentialId: string, vectorStoreId: string, file_ids: string[]) => {
try {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const deletedFileIds = []
let count = 0
for (const file of file_ids) {
const res = await openai.beta.vectorStores.files.del(vectorStoreId, file)
if (res.deleted) {
deletedFileIds.push(file)
count += 1
}
}
return { deletedFileIds, count }
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: openaiAssistantsVectorStoreService.uploadFilesToAssistantVectorStore - ${getErrorMessage(error)}`
)
}
}
export default {
getAssistantVectorStore,
listAssistantVectorStore,
createAssistantVectorStore,
updateAssistantVectorStore,
deleteAssistantVectorStore,
uploadFilesToAssistantVectorStore,
deleteFilesFromAssistantVectorStore
}
@@ -1,4 +1,5 @@
import OpenAI from 'openai'
import fs from 'fs'
import { StatusCodes } from 'http-status-codes'
import { decryptCredentialData } from '../../utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
@@ -59,8 +60,18 @@ const getSingleOpenaiAssistant = async (credentialId: string, assistantId: strin
const dbResponse = await openai.beta.assistants.retrieve(assistantId)
const resp = await openai.files.list()
const existingFiles = resp.data ?? []
if (dbResponse.file_ids && dbResponse.file_ids.length) {
;(dbResponse as any).files = existingFiles.filter((file) => dbResponse.file_ids.includes(file.id))
if (dbResponse.tool_resources?.code_interpreter?.file_ids?.length) {
;(dbResponse.tool_resources.code_interpreter as any).files = [
...existingFiles.filter((file) => dbResponse.tool_resources?.code_interpreter?.file_ids?.includes(file.id))
]
}
if (dbResponse.tool_resources?.file_search?.vector_store_ids?.length) {
// Since there can only be 1 vector store per assistant
const vectorStoreId = dbResponse.tool_resources.file_search.vector_store_ids[0]
const vectorStoreFiles = await openai.beta.vectorStores.files.list(vectorStoreId)
const fileIds = vectorStoreFiles.data?.map((file) => file.id) ?? []
;(dbResponse.tool_resources.file_search as any).files = [...existingFiles.filter((file) => fileIds.includes(file.id))]
;(dbResponse.tool_resources.file_search as any).vector_store_object = await openai.beta.vectorStores.retrieve(vectorStoreId)
}
return dbResponse
} catch (error) {
@@ -71,7 +82,38 @@ const getSingleOpenaiAssistant = async (credentialId: string, assistantId: strin
}
}
const uploadFilesToAssistant = async (credentialId: string, files: { filePath: string; fileName: string }[]) => {
const appServer = getRunningExpressApp()
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId
})
if (!credential) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Credential ${credentialId} not found in the database!`)
}
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
const openAIApiKey = decryptedCredentialData['openAIApiKey']
if (!openAIApiKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `OpenAI ApiKey not found`)
}
const openai = new OpenAI({ apiKey: openAIApiKey })
const uploadedFiles = []
for (const file of files) {
const createdFile = await openai.files.create({
file: new File([new Blob([fs.readFileSync(file.filePath)])], file.fileName),
purpose: 'assistants'
})
uploadedFiles.push(createdFile)
fs.unlinkSync(file.filePath)
}
return uploadedFiles
}
export default {
getAllOpenaiAssistants,
getSingleOpenaiAssistant
getSingleOpenaiAssistant,
uploadFilesToAssistant
}
+3
View File
@@ -1066,6 +1066,9 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
return false
}
}
// If agent is openAIAssistant, streaming is enabled
if (endingNodeData.name === 'openAIAssistant') return true
} else if (endingNodeData.category === 'Engine') {
// Engines that are available to stream
const whitelistEngine = ['contextChatEngine', 'simpleChatEngine', 'queryEngine', 'subQuestionQueryEngine']
+44 -7
View File
@@ -1,20 +1,49 @@
import client from './client'
// OpenAI Assistant
const getAssistantObj = (id, credentialId) => client.get(`/openai-assistants/${id}?credential=${credentialId}`)
const getAllAvailableAssistants = (credentialId) => client.get(`/openai-assistants?credential=${credentialId}`)
// Assistant
const createNewAssistant = (body) => client.post(`/assistants`, body)
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, isDeleteBoth) =>
isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`)
// Vector Store
const getAssistantVectorStore = (id, credentialId) => client.get(`/openai-assistants-vector-store/${id}?credential=${credentialId}`)
const listAssistantVectorStore = (credentialId) => client.get(`/openai-assistants-vector-store?credential=${credentialId}`)
const createAssistantVectorStore = (credentialId, body) => client.post(`/openai-assistants-vector-store?credential=${credentialId}`, body)
const updateAssistantVectorStore = (id, credentialId, body) =>
client.put(`/openai-assistants-vector-store/${id}?credential=${credentialId}`, body)
const deleteAssistantVectorStore = (id, credentialId) => client.delete(`/openai-assistants-vector-store/${id}?credential=${credentialId}`)
// Vector Store Files
const uploadFilesToAssistantVectorStore = (id, credentialId, formData) =>
client.post(`/openai-assistants-vector-store/${id}?credential=${credentialId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
const deleteFilesFromAssistantVectorStore = (id, credentialId, body) =>
client.patch(`/openai-assistants-vector-store/${id}?credential=${credentialId}`, body)
// Files
const uploadFilesToAssistant = (credentialId, formData) =>
client.post(`/openai-assistants-file/upload?credential=${credentialId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
export default {
getAllAssistants,
getSpecificAssistant,
@@ -22,5 +51,13 @@ export default {
getAllAvailableAssistants,
createNewAssistant,
updateAssistant,
deleteAssistant
deleteAssistant,
getAssistantVectorStore,
listAssistantVectorStore,
updateAssistantVectorStore,
createAssistantVectorStore,
uploadFilesToAssistant,
uploadFilesToAssistantVectorStore,
deleteFilesFromAssistantVectorStore,
deleteAssistantVectorStore
}
@@ -96,6 +96,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [chatMessages, setChatMessages] = useState([])
const [stats, setStats] = useState([])
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
const [selectedChatId, setSelectedChatId] = useState('')
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const [chatTypeFilter, setChatTypeFilter] = useState([])
@@ -283,6 +284,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const loadedMessages = []
for (let i = 0; i < chatmessages.length; i += 1) {
const chatmsg = chatmessages[i]
setSelectedChatId(chatmsg.chatId)
if (!prevDate) {
prevDate = chatmsg.createdDate.split('T')[0]
loadedMessages.push({
@@ -383,8 +385,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const downloadFile = async (fileAnnotation) => {
try {
const response = await axios.post(
`${baseURL}/api/v1/openai-assistants-file`,
{ fileName: fileAnnotation.fileName },
`${baseURL}/api/v1/openai-assistants-file/download`,
{ fileName: fileAnnotation.fileName, chatflowId: dialogProps.chatflow.id, chatId: selectedChatId },
{ responseType: 'blob' }
)
const blob = new Blob([response.data], { type: response.headers['content-type'] })
@@ -444,6 +446,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setChatMessages([])
setChatTypeFilter([])
setSelectedMessageIndex(0)
setSelectedChatId('')
setStartDate(new Date().setMonth(new Date().getMonth() - 1))
setEndDate(new Date())
setStats([])
@@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({
}
})
export const Dropdown = ({ name, value, options, onSelect, disabled = false, disableClearable = false }) => {
export const Dropdown = ({ name, value, loading, options, onSelect, disabled = false, disableClearable = false }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
@@ -31,6 +31,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
disabled={disabled}
disableClearable={disableClearable}
size='small'
loading={loading}
options={options || []}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
onChange={(e, selection) => {
@@ -61,6 +62,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
Dropdown.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
loading: PropTypes.bool,
options: PropTypes.array,
onSelect: PropTypes.func,
disabled: PropTypes.bool,
+45 -11
View File
@@ -5,7 +5,7 @@ import { FormControl, Button } from '@mui/material'
import { IconUpload } from '@tabler/icons'
import { getFileName } from '@/utils/genericHelper'
export const File = ({ value, fileType, onChange, disabled = false }) => {
export const File = ({ value, formDataUpload, fileType, onChange, onFormDataChange, disabled = false }) => {
const theme = useTheme()
const [myValue, setMyValue] = useState(value ?? '')
@@ -54,17 +54,43 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
}
}
const handleFormDataUpload = async (e) => {
if (!e.target.files) return
if (e.target.files.length === 1) {
const file = e.target.files[0]
const { name } = file
const formData = new FormData()
formData.append('files', file)
setMyValue(`,filename:${name}`)
onChange(`,filename:${name}`)
onFormDataChange(formData)
} else if (e.target.files.length > 0) {
const formData = new FormData()
const values = []
for (let i = 0; i < e.target.files.length; i++) {
formData.append('files', e.target.files[i])
values.push(`,filename:${e.target.files[i].name}`)
}
setMyValue(JSON.stringify(values))
onChange(JSON.stringify(values))
onFormDataChange(formData)
}
}
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<span
style={{
fontStyle: 'italic',
color: theme.palette.grey['800'],
marginBottom: '1rem'
}}
>
{myValue ? getFileName(myValue) : 'Choose a file to upload'}
</span>
{!formDataUpload && (
<span
style={{
fontStyle: 'italic',
color: theme.palette.grey['800'],
marginBottom: '1rem'
}}
>
{myValue ? getFileName(myValue) : 'Choose a file to upload'}
</span>
)}
<Button
disabled={disabled}
variant='outlined'
@@ -74,7 +100,13 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
sx={{ marginRight: '1rem' }}
>
{'Upload File'}
<input type='file' multiple accept={fileType} hidden onChange={(e) => handleFileUpload(e)} />
<input
type='file'
multiple
accept={fileType}
hidden
onChange={(e) => (formDataUpload ? handleFormDataUpload(e) : handleFileUpload(e))}
/>
</Button>
</FormControl>
)
@@ -83,6 +115,8 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
File.propTypes = {
value: PropTypes.string,
fileType: PropTypes.string,
formDataUpload: PropTypes.bool,
onChange: PropTypes.func,
onFormDataChange: PropTypes.func,
disabled: PropTypes.bool
}
+12
View File
@@ -642,3 +642,15 @@ export const getOS = () => {
return os
}
export const formatBytes = (bytes, decimals = 2) => {
if (!+bytes) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
@@ -1,11 +1,25 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { v4 as uuidv4 } from 'uuid'
import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
import {
Chip,
Card,
CardContent,
Box,
Typography,
Button,
IconButton,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
OutlinedInput
} from '@mui/material'
import { StyledButton } from '@/ui-component/button/StyledButton'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
@@ -15,9 +29,10 @@ import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
import { File } from '@/ui-component/file/File'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
import DeleteConfirmDialog from './DeleteConfirmDialog'
import AssistantVectorStoreDialog from './AssistantVectorStoreDialog'
// Icons
import { IconX } from '@tabler/icons'
import { IconX, IconPlus } from '@tabler/icons'
// API
import assistantsApi from '@/api/assistants'
@@ -28,8 +43,13 @@ import useApi from '@/hooks/useApi'
// utils
import useNotifier from '@/utils/useNotifier'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
import { maxScroll } from '@/store/constant'
const assistantAvailableModels = [
{
label: 'gpt-4-turbo',
name: 'gpt-4-turbo'
},
{
label: 'gpt-4-turbo-preview',
name: 'gpt-4-turbo-preview'
@@ -74,6 +94,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
const dispatch = useDispatch()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const customization = useSelector((state) => state.customization)
const dialogRef = useRef()
const getSpecificAssistantApi = useApi(assistantsApi.getSpecificAssistant)
const getAssistantObjApi = useApi(assistantsApi.getAssistantObj)
@@ -86,12 +108,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
const [assistantModel, setAssistantModel] = useState('')
const [assistantCredential, setAssistantCredential] = useState('')
const [assistantInstructions, setAssistantInstructions] = useState('')
const [assistantTools, setAssistantTools] = useState(['code_interpreter', 'retrieval'])
const [assistantFiles, setAssistantFiles] = useState([])
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
const [assistantTools, setAssistantTools] = useState(['code_interpreter', 'file_search'])
const [toolResources, setToolResources] = useState({})
const [temperature, setTemperature] = useState(1)
const [topP, setTopP] = useState(1)
const [uploadCodeInterpreterFiles, setUploadCodeInterpreterFiles] = useState('')
const [uploadVectorStoreFiles, setUploadVectorStoreFiles] = useState('')
const [loading, setLoading] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [deleteDialogProps, setDeleteDialogProps] = useState({})
const [assistantVectorStoreDialogOpen, setAssistantVectorStoreDialogOpen] = useState(false)
const [assistantVectorStoreDialogProps, setAssistantVectorStoreDialogProps] = useState({})
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
@@ -111,8 +138,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(assistantDetails.description)
setAssistantModel(assistantDetails.model)
setAssistantInstructions(assistantDetails.instructions)
setTemperature(assistantDetails.temperature)
setTopP(assistantDetails.top_p)
setAssistantTools(assistantDetails.tools ?? [])
setAssistantFiles(assistantDetails.files ?? [])
setToolResources(assistantDetails.tool_resources ?? {})
}
}, [getSpecificAssistantApi.data])
@@ -124,14 +153,48 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
useEffect(() => {
if (getAssistantObjApi.error) {
syncData(getAssistantObjApi.error)
let errMsg = ''
if (error?.response?.data) {
errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}
enqueueSnackbar({
message: `Failed to get assistant: ${errMsg}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantObjApi.error])
useEffect(() => {
if (getSpecificAssistantApi.error) {
syncData(getSpecificAssistantApi.error)
let errMsg = ''
if (error?.response?.data) {
errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}
enqueueSnackbar({
message: `Failed to get assistant: ${errMsg}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificAssistantApi.error])
useEffect(() => {
@@ -147,8 +210,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(assistantDetails.description)
setAssistantModel(assistantDetails.model)
setAssistantInstructions(assistantDetails.instructions)
setTemperature(assistantDetails.temperature)
setTopP(assistantDetails.top_p)
setAssistantTools(assistantDetails.tools ?? [])
setAssistantFiles(assistantDetails.files ?? [])
setToolResources(assistantDetails.tool_resources ?? {})
} else if (dialogProps.type === 'EDIT' && dialogProps.assistantId) {
// When assistant dialog is opened from OpenAIAssistant node in canvas
getSpecificAssistantApi.request(dialogProps.assistantId)
@@ -170,9 +235,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc('')
setAssistantModel('')
setAssistantInstructions('')
setAssistantTools(['code_interpreter', 'retrieval'])
setUploadAssistantFiles('')
setAssistantFiles([])
setTemperature(1)
setTopP(1)
setAssistantTools(['code_interpreter', 'file_search'])
setUploadCodeInterpreterFiles('')
setUploadVectorStoreFiles('')
setToolResources({})
}
return () => {
@@ -185,9 +253,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc('')
setAssistantModel('')
setAssistantInstructions('')
setAssistantTools(['code_interpreter', 'retrieval'])
setUploadAssistantFiles('')
setAssistantFiles([])
setTemperature(1)
setTopP(1)
setAssistantTools(['code_interpreter', 'file_search'])
setUploadCodeInterpreterFiles('')
setUploadVectorStoreFiles('')
setToolResources({})
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -199,7 +270,9 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(data.description)
setAssistantModel(data.model)
setAssistantInstructions(data.instructions)
setAssistantFiles(data.files ?? [])
setTemperature(data.temperature)
setTopP(data.top_p)
setToolResources(data.tool_resources ?? {})
let tools = []
if (data.tools && data.tools.length) {
@@ -210,6 +283,31 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantTools(tools)
}
const onEditAssistantVectorStoreClick = (vectorStoreObject) => {
const dialogProp = {
title: `Edit ${vectorStoreObject.name ? vectorStoreObject.name : vectorStoreObject.id}`,
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: vectorStoreObject,
credential: assistantCredential
}
setAssistantVectorStoreDialogProps(dialogProp)
setAssistantVectorStoreDialogOpen(true)
}
const onAddAssistantVectorStoreClick = () => {
const dialogProp = {
title: `Add Vector Store`,
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
credential: assistantCredential
}
setAssistantVectorStoreDialogProps(dialogProp)
setAssistantVectorStoreDialogOpen(true)
}
const addNewAssistant = async () => {
setLoading(true)
try {
@@ -219,9 +317,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
description: assistantDesc,
model: assistantModel,
instructions: assistantInstructions,
temperature: temperature ? parseFloat(temperature) : null,
top_p: topP ? parseFloat(topP) : null,
tools: assistantTools,
files: assistantFiles,
uploadFiles: uploadAssistantFiles
tool_resources: toolResources
}
const obj = {
details: JSON.stringify(assistantDetails),
@@ -247,7 +346,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to add new Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -264,7 +362,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
})
setLoading(false)
onCancel()
}
}
@@ -276,9 +373,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
description: assistantDesc,
model: assistantModel,
instructions: assistantInstructions,
temperature: temperature ? parseFloat(temperature) : null,
top_p: topP ? parseFloat(topP) : null,
tools: assistantTools,
files: assistantFiles,
uploadFiles: uploadAssistantFiles
tool_resources: toolResources
}
const obj = {
details: JSON.stringify(assistantDetails),
@@ -303,7 +401,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to save Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -320,7 +417,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
})
setLoading(false)
onCancel()
}
}
@@ -345,7 +441,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to sync Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -365,10 +460,124 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
}
const uploadFormDataToVectorStore = async (formData) => {
setLoading(true)
try {
const vectorStoreId = toolResources.file_search?.vector_store_ids?.length ? toolResources.file_search.vector_store_ids[0] : ''
const uploadResp = await assistantsApi.uploadFilesToAssistantVectorStore(vectorStoreId, assistantCredential, formData)
if (uploadResp.data) {
enqueueSnackbar({
message: 'File uploaded successfully!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
const uploadedFiles = uploadResp.data
const existingFiles = toolResources?.file_search.files ?? []
setToolResources({
...toolResources,
file_search: {
...toolResources?.file_search,
files: [...existingFiles, ...uploadedFiles]
}
})
}
setLoading(false)
} catch (error) {
enqueueSnackbar({
message: `Failed to upload file: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
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 uploadFormDataToCodeInterpreter = async (formData) => {
setLoading(true)
try {
const uploadResp = await assistantsApi.uploadFilesToAssistant(assistantCredential, formData)
if (uploadResp.data) {
enqueueSnackbar({
message: 'File uploaded successfully!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
const uploadedFiles = uploadResp.data
const existingFiles = toolResources?.code_interpreter?.files ?? []
const existingFileIds = toolResources?.code_interpreter?.file_ids ?? []
setToolResources({
...toolResources,
code_interpreter: {
...toolResources?.code_interpreter,
files: [...existingFiles, ...uploadedFiles],
file_ids: [...existingFileIds, ...uploadedFiles.map((file) => file.id)]
}
})
}
setLoading(false)
} catch (error) {
enqueueSnackbar({
message: `Failed to upload file: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
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 detachVectorStore = () => {
setToolResources({
...toolResources,
file_search: {
files: [],
vector_store_object: null,
vector_store_ids: []
}
})
}
const onDeleteClick = () => {
setDeleteDialogProps({
title: `Delete Assistant`,
description: `Delete Assistant ${assistantName}?`,
description: `Select delete method for ${assistantName}`,
cancelButtonName: 'Cancel'
})
setDeleteDialogOpen(true)
@@ -394,7 +603,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onConfirm()
}
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to delete Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -414,8 +622,35 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
}
const onFileDeleteClick = async (fileId) => {
setAssistantFiles(assistantFiles.filter((file) => file.id !== fileId))
const onFileDeleteClick = async (fileId, toolType) => {
if (toolType === 'code_interpreter') {
setToolResources({
...toolResources,
code_interpreter: {
...toolResources.code_interpreter,
files: toolResources.code_interpreter.files.filter((file) => file.id !== fileId),
file_ids: toolResources.code_interpreter.file_ids.filter((file_id) => file_id !== fileId)
}
})
} else if (toolType === 'file_search') {
// Remove from toolResources
setToolResources({
...toolResources,
file_search: {
...toolResources.file_search,
files: toolResources.file_search.files.filter((file) => file.id !== fileId)
}
})
// Remove files from vector store
try {
const vectorStoreId = toolResources.file_search?.vector_store_ids?.length
? toolResources.file_search.vector_store_ids[0]
: ''
await assistantsApi.deleteFilesFromAssistantVectorStore(vectorStoreId, assistantCredential, { file_ids: [fileId] })
} catch (error) {
console.error(error)
}
}
}
const component = show ? (
@@ -430,8 +665,45 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<DialogContent
ref={dialogRef}
sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
<Box>
<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>
<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>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Name</Typography>
@@ -440,6 +712,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantName'
type='string'
size='small'
fullWidth
placeholder='My New Assistant'
value={assistantName}
@@ -455,6 +728,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantDesc'
type='string'
size='small'
fullWidth
placeholder='Description of what the Assistant does'
multiline={true}
@@ -491,6 +765,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantIcon'
type='string'
size='small'
fullWidth
placeholder={`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`}
value={assistantIcon}
@@ -498,40 +773,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onChange={(e) => setAssistantIcon(e.target.value)}
/>
</Box>
<Box>
<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>
<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>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Instruction</Typography>
@@ -542,6 +783,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantInstructions'
type='string'
size='small'
fullWidth
placeholder='You are a personal math tutor. When asked a question, write and run Python code to answer the question.'
multiline={true}
@@ -553,64 +795,212 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Tools</Typography>
<TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
</Stack>
<MultiDropdown
key={JSON.stringify(assistantTools)}
name={JSON.stringify(assistantTools)}
options={[
{
label: 'Code Interpreter',
name: 'code_interpreter'
},
{
label: 'Retrieval',
name: 'retrieval'
<Typography variant='overline'>Assistant Temperature</Typography>
<TooltipWithParser
title={
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.'
}
]}
onSelect={(newValue) => (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))}
value={assistantTools ?? 'choose an option'}
/>
</Stack>
<OutlinedInput
id='assistantTemp'
type='number'
size='small'
fullWidth
value={temperature}
name='assistantTemp'
onChange={(e) => setTemperature(e.target.value)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Knowledge Files</Typography>
<TooltipWithParser title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files' />
<Typography variant='overline'>Assistant Top P</Typography>
<TooltipWithParser
title={
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered.'
}
/>
</Stack>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{assistantFiles.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>{file.filename}</span>
<IconButton sx={{ height: 15, width: 15, p: 0 }} onClick={() => onFileDeleteClick(file.id)}>
<IconX />
</IconButton>
</div>
))}
</div>
<File
key={uploadAssistantFiles}
fileType='*'
onChange={(newValue) => setUploadAssistantFiles(newValue)}
value={uploadAssistantFiles ?? 'Choose a file to upload'}
<OutlinedInput
id='assistantTopP'
type='number'
fullWidth
size='small'
value={topP}
name='assistantTopP'
min='0'
max='1'
onChange={(e) => setTopP(e.target.value)}
/>
</Box>
{assistantCredential && (
<>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Tools</Typography>
<TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
</Stack>
<MultiDropdown
key={JSON.stringify(assistantTools)}
name={JSON.stringify(assistantTools)}
options={[
{
label: 'Code Interpreter',
name: 'code_interpreter'
},
{
label: 'File Search',
name: 'file_search'
}
]}
onSelect={(newValue) => {
newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([])
setTimeout(() => {
dialogRef?.current?.scrollTo({ top: maxScroll })
}, 100)
}}
value={assistantTools ?? 'choose an option'}
/>
</Box>
<Box>
{assistantTools?.length > 0 && assistantTools.includes('code_interpreter') && (
<Card sx={{ mb: 2, border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Code Interpreter Files</Typography>
<TooltipWithParser title='Code Interpreter enables the assistant to write and run code. This tool can process files with diverse data and formatting, and generate files such as graphs' />
</Stack>
{toolResources?.code_interpreter?.files?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
{toolResources?.code_interpreter?.files?.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10,
marginBottom: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>
{file.filename}
</span>
<IconButton
sx={{ height: 15, width: 15, p: 0 }}
onClick={() => onFileDeleteClick(file.id, 'code_interpreter')}
>
<IconX />
</IconButton>
</div>
))}
</div>
)}
<File
key={uploadCodeInterpreterFiles}
fileType='*'
formDataUpload={true}
value={uploadCodeInterpreterFiles ?? 'Choose a file to upload'}
onChange={(newValue) => setUploadCodeInterpreterFiles(newValue)}
onFormDataChange={(formData) => uploadFormDataToCodeInterpreter(formData)}
/>
</CardContent>
</Card>
)}
{assistantTools?.length > 0 && assistantTools.includes('file_search') && (
<Card sx={{ mb: 2, border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>File Search Files</Typography>
<TooltipWithParser title='File search enables the assistant with knowledge from files that you or your users upload. Once a file is uploaded, the assistant automatically decides when to retrieve content based on user requests' />
</Stack>
{toolResources?.file_search?.vector_store_object && (
<Chip
label={
toolResources?.file_search?.vector_store_object?.name
? toolResources?.file_search?.vector_store_object?.name
: toolResources?.file_search?.vector_store_object?.id
}
component='a'
sx={{ mb: 2, mt: 1 }}
variant='outlined'
clickable
color='primary'
onDelete={detachVectorStore}
onClick={() =>
onEditAssistantVectorStoreClick(toolResources?.file_search?.vector_store_object)
}
/>
)}
{toolResources?.file_search?.files?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
{toolResources?.file_search?.files?.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10,
marginBottom: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>
{file.filename}
</span>
<IconButton
sx={{ height: 15, width: 15, p: 0 }}
onClick={() => onFileDeleteClick(file.id, 'file_search')}
>
<IconX />
</IconButton>
</div>
))}
</div>
)}
{!toolResources.file_search || !toolResources.file_search?.vector_store_ids?.length ? (
<Button
variant='outlined'
component='label'
fullWidth
startIcon={<IconPlus />}
sx={{ marginRight: '1rem' }}
onClick={() => onAddAssistantVectorStoreClick()}
>
Add Vector Store
</Button>
) : (
<File
key={uploadVectorStoreFiles}
fileType='*'
formDataUpload={true}
value={uploadVectorStoreFiles ?? 'Choose a file to upload'}
onChange={(newValue) => setUploadVectorStoreFiles(newValue)}
onFormDataChange={(formData) => uploadFormDataToVectorStore(formData)}
/>
)}
</CardContent>
</Card>
)}
</Box>
</>
)}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
@@ -639,6 +1029,35 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onDelete={() => deleteAssistant()}
onDeleteBoth={() => deleteAssistant(true)}
/>
<AssistantVectorStoreDialog
show={assistantVectorStoreDialogOpen}
dialogProps={assistantVectorStoreDialogProps}
onCancel={() => setAssistantVectorStoreDialogOpen(false)}
onDelete={(vectorStoreId) => {
setToolResources({
...toolResources,
file_search: {
vector_store_object: null,
files: [],
vector_store_ids: toolResources.file_search.vector_store_ids.filter((id) => vectorStoreId !== id)
}
})
setAssistantVectorStoreDialogOpen(false)
}}
onConfirm={(vectorStoreObj, files) => {
setToolResources({
...toolResources,
file_search: {
...toolResources.file_search,
vector_store_object: vectorStoreObj,
files: files ? files : toolResources.file_search?.files,
vector_store_ids: [vectorStoreObj.id]
}
})
setAssistantVectorStoreDialogOpen(false)
}}
setError={setError}
/>
{loading && <BackdropLoader open={loading} />}
</Dialog>
) : null
@@ -0,0 +1,386 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { omit } from 'lodash'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Stack, OutlinedInput, Typography } from '@mui/material'
// Project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { SwitchInput } from '@/ui-component/switch/Switch'
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
// Icons
import { IconX } from '@tabler/icons'
// API
import assistantsApi from '@/api/assistants'
// Hooks
import useApi from '@/hooks/useApi'
// utils
import useNotifier from '@/utils/useNotifier'
import { formatBytes } from '@/utils/genericHelper'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const AssistantVectorStoreDialog = ({ show, dialogProps, onCancel, onConfirm, onDelete, setError }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getAssistantVectorStoreApi = useApi(assistantsApi.getAssistantVectorStore)
const listAssistantVectorStoreApi = useApi(assistantsApi.listAssistantVectorStore)
const [name, setName] = useState('')
const [isExpirationOn, setExpirationOnOff] = useState(false)
const [expirationDays, setExpirationDays] = useState(7)
const [availableVectorStoreOptions, setAvailableVectorStoreOptions] = useState([{ label: '- Create New -', name: '-create-' }])
const [selectedVectorStore, setSelectedVectorStore] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
if (getAssistantVectorStoreApi.data) {
if (getAssistantVectorStoreApi.data.name) {
setName(getAssistantVectorStoreApi.data.name)
} else {
setName('')
}
if (getAssistantVectorStoreApi.data.id) {
setSelectedVectorStore(getAssistantVectorStoreApi.data.id)
} else {
setSelectedVectorStore('')
}
if (getAssistantVectorStoreApi.data.expires_after && getAssistantVectorStoreApi.data.expires_after.days) {
setExpirationDays(getAssistantVectorStoreApi.data.expires_after.days)
setExpirationOnOff(true)
} else {
setExpirationDays(7)
setExpirationOnOff(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantVectorStoreApi.data])
useEffect(() => {
if (listAssistantVectorStoreApi.data) {
let vectorStores = []
for (let i = 0; i < listAssistantVectorStoreApi.data.length; i += 1) {
vectorStores.push({
label: listAssistantVectorStoreApi.data[i]?.name ?? listAssistantVectorStoreApi.data[i].id,
name: listAssistantVectorStoreApi.data[i].id,
description: `${listAssistantVectorStoreApi.data[i]?.file_counts?.total} files (${formatBytes(
listAssistantVectorStoreApi.data[i]?.usage_bytes
)})`
})
}
vectorStores = vectorStores.filter((vs) => vs.name !== '-create-')
vectorStores.unshift({ label: '- Create New -', name: '-create-' })
setAvailableVectorStoreOptions(vectorStores)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listAssistantVectorStoreApi.data])
useEffect(() => {
if (getAssistantVectorStoreApi.error) {
setError(getAssistantVectorStoreApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantVectorStoreApi.error])
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
getAssistantVectorStoreApi.request(dialogProps.data.id, dialogProps.credential)
listAssistantVectorStoreApi.request(dialogProps.credential)
} else if (dialogProps.type === 'ADD') {
listAssistantVectorStoreApi.request(dialogProps.credential)
}
return () => {
setName('')
setExpirationOnOff(false)
setExpirationDays(7)
setSelectedVectorStore('')
setAvailableVectorStoreOptions([{ label: '- Create New -', name: '-create-' }])
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const deleteVectorStore = async () => {
setLoading(true)
try {
const deleteResp = await assistantsApi.deleteAssistantVectorStore(selectedVectorStore, dialogProps.credential)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Vector Store deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onDelete(selectedVectorStore)
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to delete Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const addNewVectorStore = async () => {
setLoading(true)
try {
const obj = {
name: name !== '' ? name : null,
expires_after: isExpirationOn ? { anchor: 'last_active_at', days: parseFloat(expirationDays) } : null
}
const createResp = await assistantsApi.createAssistantVectorStore(dialogProps.credential, obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Vector Store added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data)
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to add new Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const saveVectorStore = async (selectedVectorStoreId) => {
setLoading(true)
try {
const saveObj = {
name: name !== '' ? name : null,
expires_after: isExpirationOn ? { anchor: 'last_active_at', days: parseFloat(expirationDays) } : null
}
const saveResp = await assistantsApi.updateAssistantVectorStore(selectedVectorStoreId, dialogProps.credential, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Vector Store saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
if ('files' in saveResp.data) {
const files = saveResp.data.files
onConfirm(omit(saveResp.data, ['files']), files)
} else {
onConfirm(saveResp.data)
}
}
setLoading(false)
} catch (error) {
console.error('error=', error)
setError(error)
enqueueSnackbar({
message: `Failed to save Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
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'>
Select Vector Store
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
name={selectedVectorStore}
options={availableVectorStoreOptions}
loading={listAssistantVectorStoreApi.loading}
onSelect={(newValue) => {
setSelectedVectorStore(newValue)
if (newValue === '-create-') {
setName('')
setExpirationOnOff(false)
setExpirationDays(7)
} else {
getAssistantVectorStoreApi.request(newValue, dialogProps.credential)
}
}}
value={selectedVectorStore ?? 'choose an option'}
/>
</Box>
{selectedVectorStore !== '' && (
<>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Vector Store Name</Typography>
</Stack>
<OutlinedInput
id='vsName'
type='string'
fullWidth
placeholder={'My Vector Store'}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Vector Store Expiration</Typography>
</Stack>
<SwitchInput onChange={(newValue) => setExpirationOnOff(newValue)} value={isExpirationOn} />
</Box>
{isExpirationOn && (
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Expiration Days
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='expDays'
type='number'
fullWidth
value={expirationDays}
onChange={(e) => setExpirationDays(e.target.value)}
/>
</Box>
)}
</>
)}
</DialogContent>
<DialogActions>
{dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteVectorStore()}>
Delete
</StyledButton>
)}
<StyledButton
disabled={!selectedVectorStore}
variant='contained'
onClick={() => (selectedVectorStore === '-create-' ? addNewVectorStore() : saveVectorStore(selectedVectorStore))}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
{loading && <BackdropLoader open={loading} />}
</Dialog>
) : null
return createPortal(component, portalElement)
}
AssistantVectorStoreDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func,
onDelete: PropTypes.func,
setError: PropTypes.func
}
export default AssistantVectorStoreDialog
@@ -20,14 +20,13 @@ const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBo
</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
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }}>
<Button sx={{ flex: 1, mb: 1, mr: 1 }} color='error' variant='outlined' onClick={onDelete}>
Only Flowise
</Button>
<StyledButton sx={{ flex: 1, mb: 1, ml: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
OpenAI and 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>
@@ -355,6 +355,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
})
}
const updateLastMessageFileAnnotations = (fileAnnotations) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations
return allMessages
})
}
// Handle errors
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
@@ -482,8 +491,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const downloadFile = async (fileAnnotation) => {
try {
const response = await axios.post(
`${baseURL}/api/v1/openai-assistants-file`,
{ fileName: fileAnnotation.fileName },
`${baseURL}/api/v1/openai-assistants-file/download`,
{ fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId },
{ responseType: 'blob' }
)
const blob = new Blob([response.data], { type: response.headers['content-type'] })
@@ -611,6 +620,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
socket.on('usedTools', updateLastMessageUsedTools)
socket.on('fileAnnotations', updateLastMessageFileAnnotations)
socket.on('token', updateLastMessage)
}
+21445 -17341
View File
File diff suppressed because it is too large Load Diff