mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 11:00:55 +03:00
Feature/OpenAI Assistant V2 (#2258)
* add gpt4 turbo to assistant * OpenAI Assistant V2 * update langfuse handler
This commit is contained in:
+2
-1
@@ -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 ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' }}> *</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' }}> *</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' }}> *</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' }}> *</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' }}> *</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' }}> *</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)
|
||||
}
|
||||
|
||||
|
||||
Generated
+21445
-17341
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user