Feature/OpenAI Assistant V2 (#2258)

* add gpt4 turbo to assistant

* OpenAI Assistant V2

* update langfuse handler
This commit is contained in:
Henry Heng
2024-04-25 20:14:04 +01:00
committed by GitHub
parent 4782c0f6fc
commit 7360d1d9a6
25 changed files with 23422 additions and 17637 deletions
@@ -1,11 +1,25 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { v4 as uuidv4 } from 'uuid'
import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
import {
Chip,
Card,
CardContent,
Box,
Typography,
Button,
IconButton,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
OutlinedInput
} from '@mui/material'
import { StyledButton } from '@/ui-component/button/StyledButton'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
@@ -15,9 +29,10 @@ import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
import { File } from '@/ui-component/file/File'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
import DeleteConfirmDialog from './DeleteConfirmDialog'
import AssistantVectorStoreDialog from './AssistantVectorStoreDialog'
// Icons
import { IconX } from '@tabler/icons'
import { IconX, IconPlus } from '@tabler/icons'
// API
import assistantsApi from '@/api/assistants'
@@ -28,8 +43,13 @@ import useApi from '@/hooks/useApi'
// utils
import useNotifier from '@/utils/useNotifier'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
import { maxScroll } from '@/store/constant'
const assistantAvailableModels = [
{
label: 'gpt-4-turbo',
name: 'gpt-4-turbo'
},
{
label: 'gpt-4-turbo-preview',
name: 'gpt-4-turbo-preview'
@@ -74,6 +94,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
const dispatch = useDispatch()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const customization = useSelector((state) => state.customization)
const dialogRef = useRef()
const getSpecificAssistantApi = useApi(assistantsApi.getSpecificAssistant)
const getAssistantObjApi = useApi(assistantsApi.getAssistantObj)
@@ -86,12 +108,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
const [assistantModel, setAssistantModel] = useState('')
const [assistantCredential, setAssistantCredential] = useState('')
const [assistantInstructions, setAssistantInstructions] = useState('')
const [assistantTools, setAssistantTools] = useState(['code_interpreter', 'retrieval'])
const [assistantFiles, setAssistantFiles] = useState([])
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
const [assistantTools, setAssistantTools] = useState(['code_interpreter', 'file_search'])
const [toolResources, setToolResources] = useState({})
const [temperature, setTemperature] = useState(1)
const [topP, setTopP] = useState(1)
const [uploadCodeInterpreterFiles, setUploadCodeInterpreterFiles] = useState('')
const [uploadVectorStoreFiles, setUploadVectorStoreFiles] = useState('')
const [loading, setLoading] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [deleteDialogProps, setDeleteDialogProps] = useState({})
const [assistantVectorStoreDialogOpen, setAssistantVectorStoreDialogOpen] = useState(false)
const [assistantVectorStoreDialogProps, setAssistantVectorStoreDialogProps] = useState({})
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
@@ -111,8 +138,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(assistantDetails.description)
setAssistantModel(assistantDetails.model)
setAssistantInstructions(assistantDetails.instructions)
setTemperature(assistantDetails.temperature)
setTopP(assistantDetails.top_p)
setAssistantTools(assistantDetails.tools ?? [])
setAssistantFiles(assistantDetails.files ?? [])
setToolResources(assistantDetails.tool_resources ?? {})
}
}, [getSpecificAssistantApi.data])
@@ -124,14 +153,48 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
useEffect(() => {
if (getAssistantObjApi.error) {
syncData(getAssistantObjApi.error)
let errMsg = ''
if (error?.response?.data) {
errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}
enqueueSnackbar({
message: `Failed to get assistant: ${errMsg}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantObjApi.error])
useEffect(() => {
if (getSpecificAssistantApi.error) {
syncData(getSpecificAssistantApi.error)
let errMsg = ''
if (error?.response?.data) {
errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}
enqueueSnackbar({
message: `Failed to get assistant: ${errMsg}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificAssistantApi.error])
useEffect(() => {
@@ -147,8 +210,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(assistantDetails.description)
setAssistantModel(assistantDetails.model)
setAssistantInstructions(assistantDetails.instructions)
setTemperature(assistantDetails.temperature)
setTopP(assistantDetails.top_p)
setAssistantTools(assistantDetails.tools ?? [])
setAssistantFiles(assistantDetails.files ?? [])
setToolResources(assistantDetails.tool_resources ?? {})
} else if (dialogProps.type === 'EDIT' && dialogProps.assistantId) {
// When assistant dialog is opened from OpenAIAssistant node in canvas
getSpecificAssistantApi.request(dialogProps.assistantId)
@@ -170,9 +235,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc('')
setAssistantModel('')
setAssistantInstructions('')
setAssistantTools(['code_interpreter', 'retrieval'])
setUploadAssistantFiles('')
setAssistantFiles([])
setTemperature(1)
setTopP(1)
setAssistantTools(['code_interpreter', 'file_search'])
setUploadCodeInterpreterFiles('')
setUploadVectorStoreFiles('')
setToolResources({})
}
return () => {
@@ -185,9 +253,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc('')
setAssistantModel('')
setAssistantInstructions('')
setAssistantTools(['code_interpreter', 'retrieval'])
setUploadAssistantFiles('')
setAssistantFiles([])
setTemperature(1)
setTopP(1)
setAssistantTools(['code_interpreter', 'file_search'])
setUploadCodeInterpreterFiles('')
setUploadVectorStoreFiles('')
setToolResources({})
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -199,7 +270,9 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantDesc(data.description)
setAssistantModel(data.model)
setAssistantInstructions(data.instructions)
setAssistantFiles(data.files ?? [])
setTemperature(data.temperature)
setTopP(data.top_p)
setToolResources(data.tool_resources ?? {})
let tools = []
if (data.tools && data.tools.length) {
@@ -210,6 +283,31 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
setAssistantTools(tools)
}
const onEditAssistantVectorStoreClick = (vectorStoreObject) => {
const dialogProp = {
title: `Edit ${vectorStoreObject.name ? vectorStoreObject.name : vectorStoreObject.id}`,
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: vectorStoreObject,
credential: assistantCredential
}
setAssistantVectorStoreDialogProps(dialogProp)
setAssistantVectorStoreDialogOpen(true)
}
const onAddAssistantVectorStoreClick = () => {
const dialogProp = {
title: `Add Vector Store`,
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
credential: assistantCredential
}
setAssistantVectorStoreDialogProps(dialogProp)
setAssistantVectorStoreDialogOpen(true)
}
const addNewAssistant = async () => {
setLoading(true)
try {
@@ -219,9 +317,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
description: assistantDesc,
model: assistantModel,
instructions: assistantInstructions,
temperature: temperature ? parseFloat(temperature) : null,
top_p: topP ? parseFloat(topP) : null,
tools: assistantTools,
files: assistantFiles,
uploadFiles: uploadAssistantFiles
tool_resources: toolResources
}
const obj = {
details: JSON.stringify(assistantDetails),
@@ -247,7 +346,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to add new Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -264,7 +362,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
})
setLoading(false)
onCancel()
}
}
@@ -276,9 +373,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
description: assistantDesc,
model: assistantModel,
instructions: assistantInstructions,
temperature: temperature ? parseFloat(temperature) : null,
top_p: topP ? parseFloat(topP) : null,
tools: assistantTools,
files: assistantFiles,
uploadFiles: uploadAssistantFiles
tool_resources: toolResources
}
const obj = {
details: JSON.stringify(assistantDetails),
@@ -303,7 +401,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to save Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -320,7 +417,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
})
setLoading(false)
onCancel()
}
}
@@ -345,7 +441,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to sync Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -365,10 +460,124 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
}
const uploadFormDataToVectorStore = async (formData) => {
setLoading(true)
try {
const vectorStoreId = toolResources.file_search?.vector_store_ids?.length ? toolResources.file_search.vector_store_ids[0] : ''
const uploadResp = await assistantsApi.uploadFilesToAssistantVectorStore(vectorStoreId, assistantCredential, formData)
if (uploadResp.data) {
enqueueSnackbar({
message: 'File uploaded successfully!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
const uploadedFiles = uploadResp.data
const existingFiles = toolResources?.file_search.files ?? []
setToolResources({
...toolResources,
file_search: {
...toolResources?.file_search,
files: [...existingFiles, ...uploadedFiles]
}
})
}
setLoading(false)
} catch (error) {
enqueueSnackbar({
message: `Failed to upload file: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
}
}
const uploadFormDataToCodeInterpreter = async (formData) => {
setLoading(true)
try {
const uploadResp = await assistantsApi.uploadFilesToAssistant(assistantCredential, formData)
if (uploadResp.data) {
enqueueSnackbar({
message: 'File uploaded successfully!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
const uploadedFiles = uploadResp.data
const existingFiles = toolResources?.code_interpreter?.files ?? []
const existingFileIds = toolResources?.code_interpreter?.file_ids ?? []
setToolResources({
...toolResources,
code_interpreter: {
...toolResources?.code_interpreter,
files: [...existingFiles, ...uploadedFiles],
file_ids: [...existingFileIds, ...uploadedFiles.map((file) => file.id)]
}
})
}
setLoading(false)
} catch (error) {
enqueueSnackbar({
message: `Failed to upload file: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
}
}
const detachVectorStore = () => {
setToolResources({
...toolResources,
file_search: {
files: [],
vector_store_object: null,
vector_store_ids: []
}
})
}
const onDeleteClick = () => {
setDeleteDialogProps({
title: `Delete Assistant`,
description: `Delete Assistant ${assistantName}?`,
description: `Select delete method for ${assistantName}`,
cancelButtonName: 'Cancel'
})
setDeleteDialogOpen(true)
@@ -394,7 +603,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onConfirm()
}
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to delete Assistant: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@@ -414,8 +622,35 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
}
}
const onFileDeleteClick = async (fileId) => {
setAssistantFiles(assistantFiles.filter((file) => file.id !== fileId))
const onFileDeleteClick = async (fileId, toolType) => {
if (toolType === 'code_interpreter') {
setToolResources({
...toolResources,
code_interpreter: {
...toolResources.code_interpreter,
files: toolResources.code_interpreter.files.filter((file) => file.id !== fileId),
file_ids: toolResources.code_interpreter.file_ids.filter((file_id) => file_id !== fileId)
}
})
} else if (toolType === 'file_search') {
// Remove from toolResources
setToolResources({
...toolResources,
file_search: {
...toolResources.file_search,
files: toolResources.file_search.files.filter((file) => file.id !== fileId)
}
})
// Remove files from vector store
try {
const vectorStoreId = toolResources.file_search?.vector_store_ids?.length
? toolResources.file_search.vector_store_ids[0]
: ''
await assistantsApi.deleteFilesFromAssistantVectorStore(vectorStoreId, assistantCredential, { file_ids: [fileId] })
} catch (error) {
console.error(error)
}
}
}
const component = show ? (
@@ -430,8 +665,45 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<DialogContent
ref={dialogRef}
sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
OpenAI Credential
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<CredentialInputHandler
key={assistantCredential}
data={assistantCredential ? { credential: assistantCredential } : {}}
inputParam={{
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}}
onSelect={(newValue) => setAssistantCredential(newValue)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Model
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
key={assistantModel}
name={assistantModel}
options={assistantAvailableModels}
onSelect={(newValue) => setAssistantModel(newValue)}
value={assistantModel ?? 'choose an option'}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Name</Typography>
@@ -440,6 +712,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantName'
type='string'
size='small'
fullWidth
placeholder='My New Assistant'
value={assistantName}
@@ -455,6 +728,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantDesc'
type='string'
size='small'
fullWidth
placeholder='Description of what the Assistant does'
multiline={true}
@@ -491,6 +765,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantIcon'
type='string'
size='small'
fullWidth
placeholder={`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`}
value={assistantIcon}
@@ -498,40 +773,6 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onChange={(e) => setAssistantIcon(e.target.value)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Model
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
key={assistantModel}
name={assistantModel}
options={assistantAvailableModels}
onSelect={(newValue) => setAssistantModel(newValue)}
value={assistantModel ?? 'choose an option'}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
OpenAI Credential
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<CredentialInputHandler
key={assistantCredential}
data={assistantCredential ? { credential: assistantCredential } : {}}
inputParam={{
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}}
onSelect={(newValue) => setAssistantCredential(newValue)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Instruction</Typography>
@@ -542,6 +783,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
<OutlinedInput
id='assistantInstructions'
type='string'
size='small'
fullWidth
placeholder='You are a personal math tutor. When asked a question, write and run Python code to answer the question.'
multiline={true}
@@ -553,64 +795,212 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Tools</Typography>
<TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
</Stack>
<MultiDropdown
key={JSON.stringify(assistantTools)}
name={JSON.stringify(assistantTools)}
options={[
{
label: 'Code Interpreter',
name: 'code_interpreter'
},
{
label: 'Retrieval',
name: 'retrieval'
<Typography variant='overline'>Assistant Temperature</Typography>
<TooltipWithParser
title={
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.'
}
]}
onSelect={(newValue) => (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))}
value={assistantTools ?? 'choose an option'}
/>
</Stack>
<OutlinedInput
id='assistantTemp'
type='number'
size='small'
fullWidth
value={temperature}
name='assistantTemp'
onChange={(e) => setTemperature(e.target.value)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Knowledge Files</Typography>
<TooltipWithParser title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files' />
<Typography variant='overline'>Assistant Top P</Typography>
<TooltipWithParser
title={
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered.'
}
/>
</Stack>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{assistantFiles.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>{file.filename}</span>
<IconButton sx={{ height: 15, width: 15, p: 0 }} onClick={() => onFileDeleteClick(file.id)}>
<IconX />
</IconButton>
</div>
))}
</div>
<File
key={uploadAssistantFiles}
fileType='*'
onChange={(newValue) => setUploadAssistantFiles(newValue)}
value={uploadAssistantFiles ?? 'Choose a file to upload'}
<OutlinedInput
id='assistantTopP'
type='number'
fullWidth
size='small'
value={topP}
name='assistantTopP'
min='0'
max='1'
onChange={(e) => setTopP(e.target.value)}
/>
</Box>
{assistantCredential && (
<>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Tools</Typography>
<TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
</Stack>
<MultiDropdown
key={JSON.stringify(assistantTools)}
name={JSON.stringify(assistantTools)}
options={[
{
label: 'Code Interpreter',
name: 'code_interpreter'
},
{
label: 'File Search',
name: 'file_search'
}
]}
onSelect={(newValue) => {
newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([])
setTimeout(() => {
dialogRef?.current?.scrollTo({ top: maxScroll })
}, 100)
}}
value={assistantTools ?? 'choose an option'}
/>
</Box>
<Box>
{assistantTools?.length > 0 && assistantTools.includes('code_interpreter') && (
<Card sx={{ mb: 2, border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Code Interpreter Files</Typography>
<TooltipWithParser title='Code Interpreter enables the assistant to write and run code. This tool can process files with diverse data and formatting, and generate files such as graphs' />
</Stack>
{toolResources?.code_interpreter?.files?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
{toolResources?.code_interpreter?.files?.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10,
marginBottom: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>
{file.filename}
</span>
<IconButton
sx={{ height: 15, width: 15, p: 0 }}
onClick={() => onFileDeleteClick(file.id, 'code_interpreter')}
>
<IconX />
</IconButton>
</div>
))}
</div>
)}
<File
key={uploadCodeInterpreterFiles}
fileType='*'
formDataUpload={true}
value={uploadCodeInterpreterFiles ?? 'Choose a file to upload'}
onChange={(newValue) => setUploadCodeInterpreterFiles(newValue)}
onFormDataChange={(formData) => uploadFormDataToCodeInterpreter(formData)}
/>
</CardContent>
</Card>
)}
{assistantTools?.length > 0 && assistantTools.includes('file_search') && (
<Card sx={{ mb: 2, border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>File Search Files</Typography>
<TooltipWithParser title='File search enables the assistant with knowledge from files that you or your users upload. Once a file is uploaded, the assistant automatically decides when to retrieve content based on user requests' />
</Stack>
{toolResources?.file_search?.vector_store_object && (
<Chip
label={
toolResources?.file_search?.vector_store_object?.name
? toolResources?.file_search?.vector_store_object?.name
: toolResources?.file_search?.vector_store_object?.id
}
component='a'
sx={{ mb: 2, mt: 1 }}
variant='outlined'
clickable
color='primary'
onDelete={detachVectorStore}
onClick={() =>
onEditAssistantVectorStoreClick(toolResources?.file_search?.vector_store_object)
}
/>
)}
{toolResources?.file_search?.files?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
{toolResources?.file_search?.files?.map((file, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: 'max-content',
height: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 5,
paddingBottom: 5,
marginRight: 10,
marginBottom: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>
{file.filename}
</span>
<IconButton
sx={{ height: 15, width: 15, p: 0 }}
onClick={() => onFileDeleteClick(file.id, 'file_search')}
>
<IconX />
</IconButton>
</div>
))}
</div>
)}
{!toolResources.file_search || !toolResources.file_search?.vector_store_ids?.length ? (
<Button
variant='outlined'
component='label'
fullWidth
startIcon={<IconPlus />}
sx={{ marginRight: '1rem' }}
onClick={() => onAddAssistantVectorStoreClick()}
>
Add Vector Store
</Button>
) : (
<File
key={uploadVectorStoreFiles}
fileType='*'
formDataUpload={true}
value={uploadVectorStoreFiles ?? 'Choose a file to upload'}
onChange={(newValue) => setUploadVectorStoreFiles(newValue)}
onFormDataChange={(formData) => uploadFormDataToVectorStore(formData)}
/>
)}
</CardContent>
</Card>
)}
</Box>
</>
)}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
@@ -639,6 +1029,35 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
onDelete={() => deleteAssistant()}
onDeleteBoth={() => deleteAssistant(true)}
/>
<AssistantVectorStoreDialog
show={assistantVectorStoreDialogOpen}
dialogProps={assistantVectorStoreDialogProps}
onCancel={() => setAssistantVectorStoreDialogOpen(false)}
onDelete={(vectorStoreId) => {
setToolResources({
...toolResources,
file_search: {
vector_store_object: null,
files: [],
vector_store_ids: toolResources.file_search.vector_store_ids.filter((id) => vectorStoreId !== id)
}
})
setAssistantVectorStoreDialogOpen(false)
}}
onConfirm={(vectorStoreObj, files) => {
setToolResources({
...toolResources,
file_search: {
...toolResources.file_search,
vector_store_object: vectorStoreObj,
files: files ? files : toolResources.file_search?.files,
vector_store_ids: [vectorStoreObj.id]
}
})
setAssistantVectorStoreDialogOpen(false)
}}
setError={setError}
/>
{loading && <BackdropLoader open={loading} />}
</Dialog>
) : null
@@ -0,0 +1,386 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { omit } from 'lodash'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Stack, OutlinedInput, Typography } from '@mui/material'
// Project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { SwitchInput } from '@/ui-component/switch/Switch'
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
// Icons
import { IconX } from '@tabler/icons'
// API
import assistantsApi from '@/api/assistants'
// Hooks
import useApi from '@/hooks/useApi'
// utils
import useNotifier from '@/utils/useNotifier'
import { formatBytes } from '@/utils/genericHelper'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const AssistantVectorStoreDialog = ({ show, dialogProps, onCancel, onConfirm, onDelete, setError }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getAssistantVectorStoreApi = useApi(assistantsApi.getAssistantVectorStore)
const listAssistantVectorStoreApi = useApi(assistantsApi.listAssistantVectorStore)
const [name, setName] = useState('')
const [isExpirationOn, setExpirationOnOff] = useState(false)
const [expirationDays, setExpirationDays] = useState(7)
const [availableVectorStoreOptions, setAvailableVectorStoreOptions] = useState([{ label: '- Create New -', name: '-create-' }])
const [selectedVectorStore, setSelectedVectorStore] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
if (getAssistantVectorStoreApi.data) {
if (getAssistantVectorStoreApi.data.name) {
setName(getAssistantVectorStoreApi.data.name)
} else {
setName('')
}
if (getAssistantVectorStoreApi.data.id) {
setSelectedVectorStore(getAssistantVectorStoreApi.data.id)
} else {
setSelectedVectorStore('')
}
if (getAssistantVectorStoreApi.data.expires_after && getAssistantVectorStoreApi.data.expires_after.days) {
setExpirationDays(getAssistantVectorStoreApi.data.expires_after.days)
setExpirationOnOff(true)
} else {
setExpirationDays(7)
setExpirationOnOff(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantVectorStoreApi.data])
useEffect(() => {
if (listAssistantVectorStoreApi.data) {
let vectorStores = []
for (let i = 0; i < listAssistantVectorStoreApi.data.length; i += 1) {
vectorStores.push({
label: listAssistantVectorStoreApi.data[i]?.name ?? listAssistantVectorStoreApi.data[i].id,
name: listAssistantVectorStoreApi.data[i].id,
description: `${listAssistantVectorStoreApi.data[i]?.file_counts?.total} files (${formatBytes(
listAssistantVectorStoreApi.data[i]?.usage_bytes
)})`
})
}
vectorStores = vectorStores.filter((vs) => vs.name !== '-create-')
vectorStores.unshift({ label: '- Create New -', name: '-create-' })
setAvailableVectorStoreOptions(vectorStores)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listAssistantVectorStoreApi.data])
useEffect(() => {
if (getAssistantVectorStoreApi.error) {
setError(getAssistantVectorStoreApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAssistantVectorStoreApi.error])
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
getAssistantVectorStoreApi.request(dialogProps.data.id, dialogProps.credential)
listAssistantVectorStoreApi.request(dialogProps.credential)
} else if (dialogProps.type === 'ADD') {
listAssistantVectorStoreApi.request(dialogProps.credential)
}
return () => {
setName('')
setExpirationOnOff(false)
setExpirationDays(7)
setSelectedVectorStore('')
setAvailableVectorStoreOptions([{ label: '- Create New -', name: '-create-' }])
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const deleteVectorStore = async () => {
setLoading(true)
try {
const deleteResp = await assistantsApi.deleteAssistantVectorStore(selectedVectorStore, dialogProps.credential)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Vector Store deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onDelete(selectedVectorStore)
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to delete Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const addNewVectorStore = async () => {
setLoading(true)
try {
const obj = {
name: name !== '' ? name : null,
expires_after: isExpirationOn ? { anchor: 'last_active_at', days: parseFloat(expirationDays) } : null
}
const createResp = await assistantsApi.createAssistantVectorStore(dialogProps.credential, obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Vector Store added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data)
}
setLoading(false)
} catch (error) {
setError(error)
enqueueSnackbar({
message: `Failed to add new Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const saveVectorStore = async (selectedVectorStoreId) => {
setLoading(true)
try {
const saveObj = {
name: name !== '' ? name : null,
expires_after: isExpirationOn ? { anchor: 'last_active_at', days: parseFloat(expirationDays) } : null
}
const saveResp = await assistantsApi.updateAssistantVectorStore(selectedVectorStoreId, dialogProps.credential, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Vector Store saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
if ('files' in saveResp.data) {
const files = saveResp.data.files
onConfirm(omit(saveResp.data, ['files']), files)
} else {
onConfirm(saveResp.data)
}
}
setLoading(false)
} catch (error) {
console.error('error=', error)
setError(error)
enqueueSnackbar({
message: `Failed to save Vector Store: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Select Vector Store
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
name={selectedVectorStore}
options={availableVectorStoreOptions}
loading={listAssistantVectorStoreApi.loading}
onSelect={(newValue) => {
setSelectedVectorStore(newValue)
if (newValue === '-create-') {
setName('')
setExpirationOnOff(false)
setExpirationDays(7)
} else {
getAssistantVectorStoreApi.request(newValue, dialogProps.credential)
}
}}
value={selectedVectorStore ?? 'choose an option'}
/>
</Box>
{selectedVectorStore !== '' && (
<>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Vector Store Name</Typography>
</Stack>
<OutlinedInput
id='vsName'
type='string'
fullWidth
placeholder={'My Vector Store'}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Vector Store Expiration</Typography>
</Stack>
<SwitchInput onChange={(newValue) => setExpirationOnOff(newValue)} value={isExpirationOn} />
</Box>
{isExpirationOn && (
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Expiration Days
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='expDays'
type='number'
fullWidth
value={expirationDays}
onChange={(e) => setExpirationDays(e.target.value)}
/>
</Box>
)}
</>
)}
</DialogContent>
<DialogActions>
{dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteVectorStore()}>
Delete
</StyledButton>
)}
<StyledButton
disabled={!selectedVectorStore}
variant='contained'
onClick={() => (selectedVectorStore === '-create-' ? addNewVectorStore() : saveVectorStore(selectedVectorStore))}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
{loading && <BackdropLoader open={loading} />}
</Dialog>
) : null
return createPortal(component, portalElement)
}
AssistantVectorStoreDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func,
onDelete: PropTypes.func,
setError: PropTypes.func
}
export default AssistantVectorStoreDialog
@@ -20,14 +20,13 @@ const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBo
</DialogTitle>
<DialogContent>
<span>{dialogProps.description}</span>
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 20 }}>
<StyledButton sx={{ mb: 1 }} color='orange' variant='contained' onClick={onDelete}>
Delete only from Flowise
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }}>
<Button sx={{ flex: 1, mb: 1, mr: 1 }} color='error' variant='outlined' onClick={onDelete}>
Only Flowise
</Button>
<StyledButton sx={{ flex: 1, mb: 1, ml: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
OpenAI and Flowise
</StyledButton>
<StyledButton sx={{ mb: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
Delete from both OpenAI and Flowise
</StyledButton>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
</div>
</DialogContent>
</Dialog>
@@ -355,6 +355,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
})
}
const updateLastMessageFileAnnotations = (fileAnnotations) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations
return allMessages
})
}
// Handle errors
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
@@ -482,8 +491,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const downloadFile = async (fileAnnotation) => {
try {
const response = await axios.post(
`${baseURL}/api/v1/openai-assistants-file`,
{ fileName: fileAnnotation.fileName },
`${baseURL}/api/v1/openai-assistants-file/download`,
{ fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId },
{ responseType: 'blob' }
)
const blob = new Blob([response.data], { type: response.headers['content-type'] })
@@ -611,6 +620,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
socket.on('usedTools', updateLastMessageUsedTools)
socket.on('fileAnnotations', updateLastMessageFileAnnotations)
socket.on('token', updateLastMessage)
}