From bc46ccdcad3d0c140d614c9e97b013077296f0a5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 May 2024 08:01:02 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Assistants=20V2=20Support:=20Par?= =?UTF-8?q?t=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎹 fix: Autocompletion Chrome Bug on Action API Key Input chore: remove `useOriginNavigate` chore: set correct OpenAI Storage Source fix: azure file deletions, instantiate clients by source for deletion update code interpret files info feat: deleteResourceFileId chore: increase poll interval as azure easily rate limits fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records file source icons update table file filters chore: file search info and versioning fix: retrieval update with necessary tool_resources if specified fix(useMentions): add optional chaining in case listMap value is undefined fix: force assistant avatar roundedness fix: azure assistants, check correct flag chore: bump data-provider --- api/server/controllers/assistants/chatV1.js | 9 +- api/server/controllers/assistants/helpers.js | 16 +-- api/server/controllers/assistants/v2.js | 20 ++++ api/server/routes/files/files.js | 7 +- api/server/services/AssistantService.js | 2 +- .../azureAssistants/initializeClient.js | 40 +++++++ api/server/services/Files/Firebase/crud.js | 10 +- api/server/services/Files/process.js | 103 +++++++++++++----- api/server/services/Runs/handle.js | 6 +- client/src/common/types.ts | 1 + .../Chat/Input/Files/FilePreview.tsx | 3 +- .../components/Chat/Input/Files/FileRow.tsx | 9 +- .../components/Chat/Input/Files/FilesView.tsx | 13 +-- .../src/components/Chat/Input/Files/Image.tsx | 5 +- .../Chat/Input/Files/ImagePreview.tsx | 5 + .../Chat/Input/Files/SourceIcon.tsx | 45 ++++++++ .../Chat/Input/Files/Table/Columns.tsx | 23 +++- .../Chat/Input/Files/Table/DataTable.tsx | 9 +- .../src/components/Endpoints/MinimalIcon.tsx | 9 +- client/src/components/Nav/SearchBar.tsx | 1 + .../SidePanel/Builder/ActionsAuth.tsx | 2 +- .../SidePanel/Builder/AssistantSelect.tsx | 7 +- .../src/components/SidePanel/Builder/Code.tsx | 1 - .../SidePanel/Builder/CodeFiles.tsx | 17 ++- .../components/SidePanel/Builder/Images.tsx | 4 +- .../SidePanel/Builder/Retrieval.tsx | 92 ++++++++++------ .../SidePanel/Files/PanelFileCell.tsx | 19 +++- .../SidePanel/Parameters/OptionHover.tsx | 12 +- client/src/data-provider/mutations.ts | 11 +- .../hooks/Conversations/useConversation.ts | 6 +- .../Conversations/useNavigateToConvo.tsx | 6 +- client/src/hooks/Files/useFileDeletion.ts | 14 ++- client/src/hooks/Input/useMentions.ts | 8 +- client/src/hooks/index.ts | 1 - client/src/hooks/useNewConvo.ts | 7 +- client/src/hooks/useOriginNavigate.ts | 18 --- client/src/localization/languages/Eng.ts | 7 ++ client/src/mobile.css | 4 + librechat.example.yaml | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 2 +- packages/data-provider/src/data-service.ts | 3 +- .../data-provider/src/types/assistants.ts | 6 + packages/data-provider/src/types/files.ts | 7 ++ 44 files changed, 420 insertions(+), 174 deletions(-) create mode 100644 client/src/components/Chat/Input/Files/SourceIcon.tsx delete mode 100644 client/src/hooks/useOriginNavigate.ts diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index d453512c67..7da31ca84a 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -3,11 +3,11 @@ const { Constants, RunStatus, CacheKeys, - FileSources, ContentTypes, EModelEndpoint, ViolationTypes, ImageVisionTool, + checkOpenAIStorage, AssistantStreamEvents, } = require('librechat-data-provider'); const { @@ -361,10 +361,7 @@ const chatV2 = async (req, res) => { /** @type {MongoFile[]} */ const attachments = await req.body.endpointOption.attachments; - if ( - attachments && - attachments.every((attachment) => attachment.source === FileSources.openai) - ) { + if (attachments && attachments.every((attachment) => checkOpenAIStorage(attachment.source))) { return; } @@ -422,7 +419,7 @@ const chatV2 = async (req, res) => { if (processedFiles) { for (const file of processedFiles) { - if (file.source !== FileSources.openai) { + if (!checkOpenAIStorage(file.source)) { attachedFileIds.delete(file.file_id); const index = file_ids.indexOf(file.file_id); if (index > -1) { diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 9077589afd..f8c9efde47 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -1,9 +1,4 @@ -const { - EModelEndpoint, - FileSources, - CacheKeys, - defaultAssistantsVersion, -} = require('librechat-data-provider'); +const { EModelEndpoint, CacheKeys, defaultAssistantsVersion } = require('librechat-data-provider'); const { initializeClient: initAzureClient, } = require('~/server/services/Endpoints/azureAssistants'); @@ -121,13 +116,8 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que }; }; -async function getOpenAIClient({ req, res, endpointOption, initAppClient }) { - let endpoint = req.body.endpoint ?? req.query.endpoint; - if (!endpoint && req.baseUrl.includes('files') && req.body.files) { - const source = req.body.files[0]?.source; - endpoint = - source === FileSources.openai ? EModelEndpoint.assistants : EModelEndpoint.azureAssistants; - } +async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) { + let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint; const version = await getCurrentVersion(req, endpoint); if (!endpoint) { throw new Error(`[${req.baseUrl}] Endpoint is required`); diff --git a/api/server/controllers/assistants/v2.js b/api/server/controllers/assistants/v2.js index 2930d41816..c56313b792 100644 --- a/api/server/controllers/assistants/v2.js +++ b/api/server/controllers/assistants/v2.js @@ -1,3 +1,4 @@ +const { ToolCallTypes } = require('librechat-data-provider'); const { validateAndUpdateTool } = require('~/server/services/ActionService'); const { getOpenAIClient } = require('./helpers'); const { logger } = require('~/config'); @@ -54,6 +55,7 @@ const createAssistant = async (req, res) => { const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { const tools = []; + let hasFileSearch = false; for (const tool of updateData.tools ?? []) { let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool; @@ -61,6 +63,10 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { continue; } + if (actualTool.type === ToolCallTypes.FILE_SEARCH) { + hasFileSearch = true; + } + if (!actualTool.function) { tools.push(actualTool); continue; @@ -72,6 +78,20 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { } } + if (hasFileSearch && !updateData.tool_resources) { + const assistant = await openai.beta.assistants.retrieve(assistant_id); + updateData.tool_resources = assistant.tool_resources ?? null; + } + + if (hasFileSearch && !updateData.tool_resources?.file_search) { + updateData.tool_resources = { + ...(updateData.tool_resources ?? {}), + file_search: { + vector_store_ids: [], + }, + }; + } + updateData.tools = tools; if (openai.locals?.azureOptions && updateData.model) { diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 812d4bd33d..565893af3d 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -1,6 +1,6 @@ const fs = require('fs').promises; const express = require('express'); -const { isUUID, FileSources } = require('librechat-data-provider'); +const { isUUID, checkOpenAIStorage } = require('librechat-data-provider'); const { filterFile, processFileUpload, @@ -89,7 +89,7 @@ router.get('/download/:userId/:file_id', async (req, res) => { return res.status(403).send('Forbidden'); } - if (file.source === FileSources.openai && !file.model) { + if (checkOpenAIStorage(file.source) && !file.model) { logger.warn(`${errorPrefix} has no associated model: ${file_id}`); return res.status(400).send('The model used when creating this file is not available'); } @@ -110,7 +110,8 @@ router.get('/download/:userId/:file_id', async (req, res) => { let passThrough; /** @type {ReadableStream | undefined} */ let fileStream; - if (file.source === FileSources.openai) { + + if (checkOpenAIStorage(file.source)) { req.body = { model: file.model }; const { openai } = await initializeClient({ req, res }); logger.debug(`Downloading file ${file_id} from OpenAI`); diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index 51894464f7..2db0a56b6b 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -78,7 +78,7 @@ async function createOnTextProgress({ * @return {Promise} */ async function getResponse({ openai, run_id, thread_id }) { - const run = await waitForRun({ openai, run_id, thread_id, pollIntervalMs: 500 }); + const run = await waitForRun({ openai, run_id, thread_id, pollIntervalMs: 2000 }); if (run.status === RunStatus.COMPLETED) { const messages = await openai.beta.threads.messages.list(thread_id, defaultOrderQuery); diff --git a/api/server/services/Endpoints/azureAssistants/initializeClient.js b/api/server/services/Endpoints/azureAssistants/initializeClient.js index 8d225397bf..69a55c74bb 100644 --- a/api/server/services/Endpoints/azureAssistants/initializeClient.js +++ b/api/server/services/Endpoints/azureAssistants/initializeClient.js @@ -15,6 +15,44 @@ const OpenAIClient = require('~/app/clients/OpenAIClient'); const { isUserProvided } = require('~/server/utils'); const { constructAzureURL } = require('~/utils'); +class Files { + constructor(client) { + this._client = client; + } + /** + * Create an assistant file by attaching a + * [File](https://platform.openai.com/docs/api-reference/files) to an + * [assistant](https://platform.openai.com/docs/api-reference/assistants). + */ + create(assistantId, body, options) { + return this._client.post(`/assistants/${assistantId}/files`, { + body, + ...options, + headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers }, + }); + } + + /** + * Retrieves an AssistantFile. + */ + retrieve(assistantId, fileId, options) { + return this._client.get(`/assistants/${assistantId}/files/${fileId}`, { + ...options, + headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers }, + }); + } + + /** + * Delete an assistant file. + */ + del(assistantId, fileId, options) { + return this._client.delete(`/assistants/${assistantId}/files/${fileId}`, { + ...options, + headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers }, + }); + } +} + const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => { const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } = process.env; @@ -130,6 +168,8 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie ...opts, }); + openai.beta.assistants.files = new Files(openai); + openai.req = req; openai.res = res; diff --git a/api/server/services/Files/Firebase/crud.js b/api/server/services/Files/Firebase/crud.js index 43b5ec9b25..c4d1d05bf6 100644 --- a/api/server/services/Files/Firebase/crud.js +++ b/api/server/services/Files/Firebase/crud.js @@ -180,7 +180,15 @@ const deleteFirebaseFile = async (req, file) => { if (!fileName.includes(req.user.id)) { throw new Error('Invalid file path'); } - await deleteFile('', fileName); + try { + await deleteFile('', fileName); + } catch (error) { + logger.error('Error deleting file from Firebase:', error); + if (error.code === 'storage/object-not-found') { + return; + } + throw error; + } }; /** diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index f1098b93f7..197fd160cf 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -10,21 +10,19 @@ const { EModelEndpoint, mergeFileConfig, hostImageIdSuffix, + checkOpenAIStorage, hostImageNamePrefix, isAssistantsEndpoint, } = require('librechat-data-provider'); +const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2'); const { convertImage, resizeAndConvert } = require('~/server/services/Files/images'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); -const { addResourceFileId } = require('~/server/controllers/assistants/v2'); const { LB_QueueAsyncCall } = require('~/server/utils/queue'); const { getStrategyFunctions } = require('./strategies'); const { determineFileType } = require('~/server/utils'); const { logger } = require('~/config'); -const checkOpenAIStorage = (source) => - source === FileSources.openai || source === FileSources.azure; - const processFiles = async (files) => { const promises = []; for (let file of files) { @@ -39,13 +37,15 @@ const processFiles = async (files) => { /** * Enqueues the delete operation to the leaky bucket queue if necessary, or adds it directly to promises. * - * @param {Express.Request} req - The express request object. - * @param {MongoFile} file - The file object to delete. - * @param {Function} deleteFile - The delete file function. - * @param {Promise[]} promises - The array of promises to await. - * @param {OpenAI | undefined} [openai] - If an OpenAI file, the initialized OpenAI client. + * @param {object} params - The passed parameters. + * @param {Express.Request} params.req - The express request object. + * @param {MongoFile} params.file - The file object to delete. + * @param {Function} params.deleteFile - The delete file function. + * @param {Promise[]} params.promises - The array of promises to await. + * @param {string[]} params.resolvedFileIds - The array of promises to await. + * @param {OpenAI | undefined} [params.openai] - If an OpenAI file, the initialized OpenAI client. */ -function enqueueDeleteOperation(req, file, deleteFile, promises, openai) { +function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai }) { if (checkOpenAIStorage(file.source)) { // Enqueue to leaky bucket promises.push( @@ -58,6 +58,7 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) { logger.error('Error deleting file from OpenAI source', err); reject(err); } else { + resolvedFileIds.push(file.file_id); resolve(result); } }, @@ -67,10 +68,12 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) { } else { // Add directly to promises promises.push( - deleteFile(req, file).catch((err) => { - logger.error('Error deleting file', err); - return Promise.reject(err); - }), + deleteFile(req, file) + .then(() => resolvedFileIds.push(file.file_id)) + .catch((err) => { + logger.error('Error deleting file', err); + return Promise.reject(err); + }), ); } } @@ -85,35 +88,71 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) { * @param {Express.Request} params.req - The express request object. * @param {DeleteFilesBody} params.req.body - The request body. * @param {string} [params.req.body.assistant_id] - The assistant ID if file uploaded is associated to an assistant. + * @param {string} [params.req.body.tool_resource] - The tool resource if assistant file uploaded is associated to a tool resource. * * @returns {Promise} */ const processDeleteRequest = async ({ req, files }) => { - const file_ids = files.map((file) => file.file_id); - + const resolvedFileIds = []; const deletionMethods = {}; const promises = []; - promises.push(deleteFiles(file_ids)); - /** @type {OpenAI | undefined} */ - let openai; - if (req.body.assistant_id) { - ({ openai } = await getOpenAIClient({ req })); + /** @type {Record} */ + const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined }; + const initializeClients = async () => { + const openAIClient = await getOpenAIClient({ + req, + overrideEndpoint: EModelEndpoint.assistants, + }); + client[FileSources.openai] = openAIClient.openai; + + if (!req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) { + return; + } + + const azureClient = await getOpenAIClient({ + req, + overrideEndpoint: EModelEndpoint.azureAssistants, + }); + client[FileSources.azure] = azureClient.openai; + }; + + if (req.body.assistant_id !== undefined) { + await initializeClients(); } for (const file of files) { const source = file.source ?? FileSources.local; - if (checkOpenAIStorage(source) && !openai) { - ({ openai } = await getOpenAIClient({ req })); + if (checkOpenAIStorage(source) && !client[source]) { + await initializeClients(); } - if (req.body.assistant_id) { + const openai = client[source]; + + if (req.body.assistant_id && req.body.tool_resource) { + promises.push( + deleteResourceFileId({ + req, + openai, + file_id: file.file_id, + assistant_id: req.body.assistant_id, + tool_resource: req.body.tool_resource, + }), + ); + } else if (req.body.assistant_id) { promises.push(openai.beta.assistants.files.del(req.body.assistant_id, file.file_id)); } if (deletionMethods[source]) { - enqueueDeleteOperation(req, file, deletionMethods[source], promises, openai); + enqueueDeleteOperation({ + req, + file, + deleteFile: deletionMethods[source], + promises, + resolvedFileIds, + openai, + }); continue; } @@ -123,10 +162,11 @@ const processDeleteRequest = async ({ req, files }) => { } deletionMethods[source] = deleteFile; - enqueueDeleteOperation(req, file, deleteFile, promises, openai); + enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai }); } await Promise.allSettled(promises); + await deleteFiles(resolvedFileIds); }; /** @@ -381,7 +421,10 @@ const processOpenAIFile = async ({ originalName ? `/${originalName}` : '' }`; const type = mime.getType(originalName ?? file_id); - + const source = + openai.req.body.endpoint === EModelEndpoint.azureAssistants + ? FileSources.azure + : FileSources.openai; const file = { ..._file, type, @@ -390,7 +433,7 @@ const processOpenAIFile = async ({ usage: 1, user: userId, context: _file.purpose, - source: FileSources.openai, + source, model: openai.req.body.model, filename: originalName ?? file_id, }; @@ -435,12 +478,14 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx filename: `${hostImageNamePrefix}${filename}`, }; createFile(file, true); + const source = + req.body.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai; createFile( { ...file, file_id, filename, - source: FileSources.openai, + source, type: mime.getType(fileExt), }, true, diff --git a/api/server/services/Runs/handle.js b/api/server/services/Runs/handle.js index 8b73b099ee..dd048219bb 100644 --- a/api/server/services/Runs/handle.js +++ b/api/server/services/Runs/handle.js @@ -55,7 +55,7 @@ async function createRun({ openai, thread_id, body }) { * @param {string} params.run_id - The ID of the run to wait for. * @param {string} params.thread_id - The ID of the thread associated with the run. * @param {RunManager} params.runManager - The RunManager instance to manage run steps. - * @param {number} [params.pollIntervalMs=750] - The interval for polling the run status; default is 750 milliseconds. + * @param {number} [params.pollIntervalMs=2000] - The interval for polling the run status; default is 2000 milliseconds. * @param {number} [params.timeout=180000] - The period to wait until timing out polling; default is 3 minutes (in ms). * @return {Promise} A promise that resolves to the last fetched run object. */ @@ -64,7 +64,7 @@ async function waitForRun({ run_id, thread_id, runManager, - pollIntervalMs = 750, + pollIntervalMs = 2000, timeout = 60000 * 3, }) { let timeElapsed = 0; @@ -233,7 +233,7 @@ async function _handleRun({ openai, run_id, thread_id }) { run_id, thread_id, runManager, - pollIntervalMs: 750, + pollIntervalMs: 2000, timeout: 60000, }); const actions = []; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 95ff597aac..62aae7f14b 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -326,6 +326,7 @@ export type IconProps = Pick & iconURL?: string; message?: boolean; className?: string; + iconClassName?: string; endpoint?: EModelEndpoint | string | null; endpointType?: EModelEndpoint | null; assistantName?: string; diff --git a/client/src/components/Chat/Input/Files/FilePreview.tsx b/client/src/components/Chat/Input/Files/FilePreview.tsx index b87f514740..e1060e8978 100644 --- a/client/src/components/Chat/Input/Files/FilePreview.tsx +++ b/client/src/components/Chat/Input/Files/FilePreview.tsx @@ -2,6 +2,7 @@ import type { TFile } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import FileIcon from '~/components/svg/Files/FileIcon'; import ProgressCircle from './ProgressCircle'; +import SourceIcon from './SourceIcon'; import { useProgress } from '~/hooks'; import { cn } from '~/utils'; @@ -21,7 +22,6 @@ const FilePreview = ({ const radius = 55; // Radius of the SVG circle const circumference = 2 * Math.PI * radius; const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1); - console.log(progress); // Calculate the offset based on the loading progress const offset = circumference - progress * circumference; @@ -32,6 +32,7 @@ const FilePreview = ({ return (
+ {progress < 1 && ( >; fileFilter?: (file: ExtendedFile) => boolean; assistant_id?: string; + tool_resource?: EToolResources; Wrapper?: React.FC<{ children: React.ReactNode }>; }) { const files = Array.from(_files.values()).filter((file) => @@ -25,7 +28,8 @@ export default function FileRow({ ); const { mutateAsync } = useDeleteFilesMutation({ - onMutate: async () => console.log('Deleting files: assistant_id', assistant_id), + onMutate: async () => + console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource), onSuccess: () => { console.log('Files deleted'); }, @@ -34,7 +38,7 @@ export default function FileRow({ }, }); - const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id }); + const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource }); useEffect(() => { if (!files) { @@ -82,6 +86,7 @@ export default function FileRow({ url={file.preview} onDelete={handleDelete} progress={file.progress} + source={file.source} /> ); } diff --git a/client/src/components/Chat/Input/Files/FilesView.tsx b/client/src/components/Chat/Input/Files/FilesView.tsx index efd9ec2a82..8791e6c915 100644 --- a/client/src/components/Chat/Input/Files/FilesView.tsx +++ b/client/src/components/Chat/Input/Files/FilesView.tsx @@ -12,16 +12,9 @@ export default function Files({ open, onOpenChange }) { const { data: files = [] } = useGetFiles({ select: (files) => files.map((file) => { - if (file.source === FileSources.local || file.source === FileSources.openai) { - file.context = file.context ?? FileContext.unknown; - return file; - } else { - return { - ...file, - context: file.context ?? FileContext.unknown, - source: FileSources.local, - }; - } + file.context = file.context ?? FileContext.unknown; + file.filterSource = file.source === FileSources.firebase ? FileSources.local : file.source; + return file; }), }); diff --git a/client/src/components/Chat/Input/Files/Image.tsx b/client/src/components/Chat/Input/Files/Image.tsx index 1cd13c8332..22c03b5373 100644 --- a/client/src/components/Chat/Input/Files/Image.tsx +++ b/client/src/components/Chat/Input/Files/Image.tsx @@ -1,3 +1,4 @@ +import { FileSources } from 'librechat-data-provider'; import ImagePreview from './ImagePreview'; import RemoveFile from './RemoveFile'; @@ -6,16 +7,18 @@ const Image = ({ url, onDelete, progress = 1, + source = FileSources.local, }: { imageBase64?: string; url?: string; onDelete: () => void; progress: number; // between 0 and 1 + source?: FileSources; }) => { return (
- +
diff --git a/client/src/components/Chat/Input/Files/ImagePreview.tsx b/client/src/components/Chat/Input/Files/ImagePreview.tsx index 4794812358..2876c2aef7 100644 --- a/client/src/components/Chat/Input/Files/ImagePreview.tsx +++ b/client/src/components/Chat/Input/Files/ImagePreview.tsx @@ -1,4 +1,6 @@ +import { FileSources } from 'librechat-data-provider'; import ProgressCircle from './ProgressCircle'; +import SourceIcon from './SourceIcon'; import { cn } from '~/utils'; type styleProps = { @@ -13,11 +15,13 @@ const ImagePreview = ({ url, progress = 1, className = '', + source, }: { imageBase64?: string; url?: string; progress?: number; // between 0 and 1 className?: string; + source?: FileSources; }) => { let style: styleProps = { backgroundSize: 'cover', @@ -65,6 +69,7 @@ const ImagePreview = ({ circleCSSProperties={circleCSSProperties} /> )} +
); }; diff --git a/client/src/components/Chat/Input/Files/SourceIcon.tsx b/client/src/components/Chat/Input/Files/SourceIcon.tsx new file mode 100644 index 0000000000..23cc4d8166 --- /dev/null +++ b/client/src/components/Chat/Input/Files/SourceIcon.tsx @@ -0,0 +1,45 @@ +import { EModelEndpoint, FileSources } from 'librechat-data-provider'; +import { MinimalIcon } from '~/components/Endpoints'; +import { cn } from '~/utils'; + +const sourceToEndpoint = { + [FileSources.openai]: EModelEndpoint.openAI, + [FileSources.azure]: EModelEndpoint.azureOpenAI, +}; +const sourceToClassname = { + [FileSources.openai]: 'bg-black/65', + [FileSources.azure]: 'azure-bg-color opacity-85', +}; + +const defaultClassName = + 'absolute right-0 bottom-0 rounded-full p-[0.15rem] text-gray-600 transition-colors'; + +export default function SourceIcon({ + source, + className = defaultClassName, +}: { + source?: FileSources; + className?: string; +}) { + if (source === FileSources.local || source === FileSources.firebase) { + return null; + } + + const endpoint = sourceToEndpoint[source ?? '']; + + if (!endpoint) { + return null; + } + return ( + + ); +} diff --git a/client/src/components/Chat/Input/Files/Table/Columns.tsx b/client/src/components/Chat/Input/Files/Table/Columns.tsx index 5b53a06f46..7284f29310 100644 --- a/client/src/components/Chat/Input/Files/Table/Columns.tsx +++ b/client/src/components/Chat/Input/Files/Table/Columns.tsx @@ -7,6 +7,7 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview'; import FilePreview from '~/components/Chat/Input/Files/FilePreview'; import { SortFilterHeader } from './SortFilterHeader'; import { OpenAIMinimalIcon } from '~/components/svg'; +import { AzureMinimalIcon } from '~/components/svg'; import { Button, Checkbox } from '~/components/ui'; import { formatDate, getFileType } from '~/utils'; import useLocalize from '~/hooks/useLocalize'; @@ -71,10 +72,11 @@ export const columns: ColumnDef[] = [ const file = row.original; if (file.type?.startsWith('image')) { return ( -
+
{file.filename}
@@ -84,7 +86,7 @@ export const columns: ColumnDef[] = [ const fileType = getFileType(file.type); return (
- {fileType && } + {fileType && } {file.filename}
); @@ -108,7 +110,7 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => formatDate(row.original.updatedAt), }, { - accessorKey: 'source', + accessorKey: 'filterSource', header: ({ column }) => { const localize = useLocalize(); return ( @@ -117,10 +119,14 @@ export const columns: ColumnDef[] = [ title={localize('com_ui_storage')} filters={{ Storage: Object.values(FileSources).filter( - (value) => value === FileSources.local || value === FileSources.openai, + (value) => + value === FileSources.local || + value === FileSources.openai || + value === FileSources.azure, ), }} valueMap={{ + [FileSources.azure]: 'Azure', [FileSources.openai]: 'OpenAI', [FileSources.local]: 'com_ui_host', }} @@ -137,6 +143,13 @@ export const columns: ColumnDef[] = [ {'OpenAI'}
); + } else if (source === FileSources.azure) { + return ( +
+ + {'Azure'} +
+ ); } return (
diff --git a/client/src/components/Chat/Input/Files/Table/DataTable.tsx b/client/src/components/Chat/Input/Files/Table/DataTable.tsx index 347006b484..1886ffc875 100644 --- a/client/src/components/Chat/Input/Files/Table/DataTable.tsx +++ b/client/src/components/Chat/Input/Files/Table/DataTable.tsx @@ -48,7 +48,12 @@ const contextMap = { [FileContext.bytes]: 'com_ui_size', }; -type Style = { width?: number | string; maxWidth?: number | string; minWidth?: number | string }; +type Style = { + width?: number | string; + maxWidth?: number | string; + minWidth?: number | string; + zIndex?: number; +}; export default function DataTable({ columns, data }: DataTableProps) { const localize = useLocalize(); @@ -142,7 +147,7 @@ export default function DataTable({ columns, data }: DataTablePro {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, index) => { - const style: Style = { maxWidth: '32px', minWidth: '125px' }; + const style: Style = { maxWidth: '32px', minWidth: '125px', zIndex: 50 }; if (header.id === 'filename') { style.maxWidth = '50%'; style.width = '50%'; diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx index ea4a74e86b..1bb200ced3 100644 --- a/client/src/components/Endpoints/MinimalIcon.tsx +++ b/client/src/components/Endpoints/MinimalIcon.tsx @@ -15,7 +15,7 @@ import { cn } from '~/utils'; import { IconProps } from '~/common'; const MinimalIcon: React.FC = (props) => { - const { size = 30, error } = props; + const { size = 30, iconClassName, error } = props; let endpoint = 'default'; // Default value for endpoint @@ -25,10 +25,13 @@ const MinimalIcon: React.FC = (props) => { const endpointIcons = { [EModelEndpoint.azureOpenAI]: { - icon: , + icon: , + name: props.chatGptLabel || 'ChatGPT', + }, + [EModelEndpoint.openAI]: { + icon: , name: props.chatGptLabel || 'ChatGPT', }, - [EModelEndpoint.openAI]: { icon: , name: props.chatGptLabel || 'ChatGPT' }, [EModelEndpoint.gptPlugins]: { icon: , name: 'Plugins' }, [EModelEndpoint.google]: { icon: , name: props.modelLabel || 'Google' }, [EModelEndpoint.anthropic]: { diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 5518fb2865..e8a02a952f 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -71,6 +71,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = }} placeholder={localize('com_nav_search_placeholder')} onKeyUp={handleKeyUp} + autoComplete="off" /> { diff --git a/client/src/components/SidePanel/Builder/AssistantSelect.tsx b/client/src/components/SidePanel/Builder/AssistantSelect.tsx index d0d6405ff0..a885123441 100644 --- a/client/src/components/SidePanel/Builder/AssistantSelect.tsx +++ b/client/src/components/SidePanel/Builder/AssistantSelect.tsx @@ -4,6 +4,7 @@ import { Tools, FileSources, Capabilities, + EModelEndpoint, LocalStorageKeys, isImageVisionTool, defaultAssistantFormValues, @@ -52,6 +53,8 @@ export default function AssistantSelect({ const assistants = useListAssistantsQuery(endpoint, undefined, { select: (res) => res.data.map((_assistant) => { + const source = + endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure; const assistant = { ..._assistant, label: _assistant?.name ?? '', @@ -77,7 +80,7 @@ export default function AssistantSelect({ size: file.bytes, preview: file.filepath, progress: 1, - source: FileSources.openai, + source, }, ]); } else { @@ -90,7 +93,7 @@ export default function AssistantSelect({ size: 1, progress: 1, filepath: endpoint, - source: FileSources.openai, + source, }, ]); } diff --git a/client/src/components/SidePanel/Builder/Code.tsx b/client/src/components/SidePanel/Builder/Code.tsx index 9fecfce77b..171e7f5048 100644 --- a/client/src/components/SidePanel/Builder/Code.tsx +++ b/client/src/components/SidePanel/Builder/Code.tsx @@ -63,7 +63,6 @@ export default function Code({ version={version} endpoint={endpoint} files={files} - tool_resource={Capabilities.code_interpreter} /> )} diff --git a/client/src/components/SidePanel/Builder/CodeFiles.tsx b/client/src/components/SidePanel/Builder/CodeFiles.tsx index 60bb735c23..a405ddb6b4 100644 --- a/client/src/components/SidePanel/Builder/CodeFiles.tsx +++ b/client/src/components/SidePanel/Builder/CodeFiles.tsx @@ -1,5 +1,9 @@ import { useState, useRef, useEffect } from 'react'; -import { mergeFileConfig, fileConfig as defaultFileConfig } from 'librechat-data-provider'; +import { + EToolResources, + mergeFileConfig, + fileConfig as defaultFileConfig, +} from 'librechat-data-provider'; import type { AssistantsEndpoint } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import FileRow from '~/components/Chat/Input/Files/FileRow'; @@ -8,17 +12,17 @@ import { useFileHandling } from '~/hooks/Files'; import useLocalize from '~/hooks/useLocalize'; import { useChatContext } from '~/Providers'; -export default function Knowledge({ +const tool_resource = EToolResources.code_interpreter; + +export default function CodeFiles({ endpoint, assistant_id, files: _files, - tool_resource, }: { version: number | string; endpoint: AssistantsEndpoint; assistant_id: string; files?: [string, ExtendedFile][]; - tool_resource?: string; }) { const localize = useLocalize(); const { setFilesLoading } = useChatContext(); @@ -57,13 +61,14 @@ export default function Knowledge({
- {assistant_id ? localize('com_assistants_knowledge_info') : ''} + {localize('com_assistants_code_interpreter_files')}
{children}
} />
diff --git a/client/src/components/SidePanel/Builder/Images.tsx b/client/src/components/SidePanel/Builder/Images.tsx index 8201ed0eeb..502ec2f907 100644 --- a/client/src/components/SidePanel/Builder/Images.tsx +++ b/client/src/components/SidePanel/Builder/Images.tsx @@ -41,10 +41,10 @@ export const AssistantAvatar = ({ return (
-
+
GPT { + const vectorStores = useMemo(() => { if (typeof assistant === 'string') { return []; } return assistant.tool_resources?.file_search; }, [assistant]); + const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]); + useEffect(() => { - if (model && !retrievalModels.has(model)) { + if (model && isDisabled) { setValue(Capabilities.retrieval, false); } - }, [model, setValue, retrievalModels]); + }, [model, setValue, isDisabled]); return ( -
- ( - +
+ ( + + )} + /> + + + + + - )} - /> - -
+ +
+ {version == 2 && ( +
+ {localize('com_assistants_file_search_info')} +
+ )} + ); } diff --git a/client/src/components/SidePanel/Files/PanelFileCell.tsx b/client/src/components/SidePanel/Files/PanelFileCell.tsx index 4d5d02979c..979d45f609 100644 --- a/client/src/components/SidePanel/Files/PanelFileCell.tsx +++ b/client/src/components/SidePanel/Files/PanelFileCell.tsx @@ -1,8 +1,10 @@ import { useCallback } from 'react'; import { fileConfig as defaultFileConfig, + checkOpenAIStorage, mergeFileConfig, megabyte, + isAssistantsEndpoint, } from 'librechat-data-provider'; import type { Row } from '@tanstack/react-table'; import type { TFile } from 'librechat-data-provider'; @@ -36,6 +38,18 @@ export default function PanelFileCell({ row }: { row: Row }) { return showToast({ message: localize('com_ui_attach_error'), status: 'error' }); } + if (checkOpenAIStorage(fileData?.source ?? '') && !isAssistantsEndpoint(endpoint)) { + return showToast({ + message: localize('com_ui_attach_error_openai'), + status: 'error', + }); + } else if (!checkOpenAIStorage(fileData?.source ?? '') && isAssistantsEndpoint(endpoint)) { + showToast({ + message: localize('com_ui_attach_warn_endpoint'), + status: 'warning', + }); + } + const { fileSizeLimit, supportedMimeTypes } = fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default; @@ -81,7 +95,8 @@ export default function PanelFileCell({ row }: { row: Row }) { > {file.filename}
@@ -94,7 +109,7 @@ export default function PanelFileCell({ row }: { row: Row }) { onClick={handleFileClick} className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700" > - {fileType && } + {fileType && } {file.filename}
); diff --git a/client/src/components/SidePanel/Parameters/OptionHover.tsx b/client/src/components/SidePanel/Parameters/OptionHover.tsx index 1a3714a0dc..16e33fff92 100644 --- a/client/src/components/SidePanel/Parameters/OptionHover.tsx +++ b/client/src/components/SidePanel/Parameters/OptionHover.tsx @@ -7,11 +7,21 @@ type TOptionHoverProps = { description: string; langCode?: boolean; sideOffset?: number; + disabled?: boolean; side: ESide; }; -function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) { +function OptionHover({ + side, + description, + disabled, + langCode, + sideOffset = 30, +}: TOptionHoverProps) { const localize = useLocalize(); + if (disabled) { + return null; + } const text = langCode ? localize(description) : description; return ( diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index 58557ea1d3..01e3a45ecb 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -1,4 +1,8 @@ -import { Capabilities, LocalStorageKeys, defaultAssistantsVersion } from 'librechat-data-provider'; +import { + EToolResources, + LocalStorageKeys, + defaultAssistantsVersion, +} from 'librechat-data-provider'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query'; import type t from 'librechat-data-provider'; @@ -420,7 +424,7 @@ export const useUploadFileMutation = ( if (!tool_resource) { update['file_ids'] = [...assistant.file_ids, data.file_id]; } - if (tool_resource === Capabilities.code_interpreter) { + if (tool_resource === EToolResources.code_interpreter) { const prevResources = assistant.tool_resources ?? {}; const prevResource = assistant.tool_resources?.[tool_resource as string] ?? { file_ids: [], @@ -455,7 +459,8 @@ export const useDeleteFilesMutation = ( const queryClient = useQueryClient(); const { onSuccess, ...options } = _options || {}; return useMutation([MutationKeys.fileDelete], { - mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body.files, body.assistant_id), + mutationFn: (body: t.DeleteFilesBody) => + dataService.deleteFiles(body.files, body.assistant_id, body.tool_resource), ...(options || {}), onSuccess: (data, ...args) => { queryClient.setQueryData([QueryKeys.files], (cachefiles) => { diff --git a/client/src/hooks/Conversations/useConversation.ts b/client/src/hooks/Conversations/useConversation.ts index 1a977f1456..7f58a136f9 100644 --- a/client/src/hooks/Conversations/useConversation.ts +++ b/client/src/hooks/Conversations/useConversation.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query'; import type { @@ -10,11 +11,10 @@ import type { TEndpointsConfig, } from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; -import useOriginNavigate from '../useOriginNavigate'; import store from '~/store'; const useConversation = () => { - const navigate = useOriginNavigate(); + const navigate = useNavigate(); const setConversation = useSetRecoilState(store.conversation); const resetLatestMessage = useResetRecoilState(store.latestMessage); const setMessages = useSetRecoilState(store.messages); @@ -59,7 +59,7 @@ const useConversation = () => { resetLatestMessage(); if (conversation.conversationId === 'new' && !modelsData) { - navigate('new'); + navigate('/c/new'); } }, [endpointsConfig, modelsQuery.data], diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index f2384becac..17f2563ab4 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -1,14 +1,14 @@ +import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { useSetRecoilState, useResetRecoilState } from 'recoil'; import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; -import useOriginNavigate from '../useOriginNavigate'; import store from '~/store'; const useNavigateToConvo = (index = 0) => { + const navigate = useNavigate(); const queryClient = useQueryClient(); - const navigate = useOriginNavigate(); const { setConversation } = store.useCreateConversationAtom(index); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index)); @@ -48,7 +48,7 @@ const useNavigateToConvo = (index = 0) => { }); } setConversation(convo); - navigate(convo?.conversationId); + navigate(`/c/${convo.conversationId ?? 'new'}`); }; const navigateWithLastTools = (conversation: TConversation) => { diff --git a/client/src/hooks/Files/useFileDeletion.ts b/client/src/hooks/Files/useFileDeletion.ts index 81a46fbdcd..13eb5dd4c4 100644 --- a/client/src/hooks/Files/useFileDeletion.ts +++ b/client/src/hooks/Files/useFileDeletion.ts @@ -1,5 +1,5 @@ import debounce from 'lodash/debounce'; -import { FileSources } from 'librechat-data-provider'; +import { FileSources, EToolResources } from 'librechat-data-provider'; import { useCallback, useState, useEffect } from 'react'; import type { BatchFile, @@ -16,18 +16,20 @@ type FileMapSetter = GenericSetter>; const useFileDeletion = ({ mutateAsync, assistant_id, + tool_resource, }: { mutateAsync: UseMutateAsyncFunction; assistant_id?: string; + tool_resource?: EToolResources; }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_batch, setFileDeleteBatch] = useState([]); const setFilesToDelete = useSetFilesToDelete(); const executeBatchDelete = useCallback( - (filesToDelete: BatchFile[], assistant_id?: string) => { - console.log('Deleting files:', filesToDelete, assistant_id); - mutateAsync({ files: filesToDelete, assistant_id }); + (filesToDelete: BatchFile[], assistant_id?: string, tool_resource?: EToolResources) => { + console.log('Deleting files:', filesToDelete, assistant_id, tool_resource); + mutateAsync({ files: filesToDelete, assistant_id, tool_resource }); setFileDeleteBatch([]); }, [mutateAsync], @@ -81,11 +83,11 @@ const useFileDeletion = ({ setFileDeleteBatch((prevBatch) => { const newBatch = [...prevBatch, file]; - debouncedDelete(newBatch, assistant_id); + debouncedDelete(newBatch, assistant_id, tool_resource); return newBatch; }); }, - [debouncedDelete, setFilesToDelete, assistant_id], + [debouncedDelete, setFilesToDelete, assistant_id, tool_resource], ); const deleteFiles = useCallback( diff --git a/client/src/hooks/Input/useMentions.ts b/client/src/hooks/Input/useMentions.ts index cf346a8c44..3cb616acae 100644 --- a/client/src/hooks/Input/useMentions.ts +++ b/client/src/hooks/Input/useMentions.ts @@ -58,23 +58,23 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant const assistantListMap = useMemo( () => ({ [EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants] - .map( + ?.map( assistantMapFn({ endpoint: EModelEndpoint.assistants, assistantMap, endpointsConfig, }), ) - .filter(Boolean), + ?.filter(Boolean), [EModelEndpoint.azureAssistants]: listMap[EModelEndpoint.azureAssistants] - .map( + ?.map( assistantMapFn({ endpoint: EModelEndpoint.azureAssistants, assistantMap, endpointsConfig, }), ) - .filter(Boolean), + ?.filter(Boolean), }), [listMap, assistantMap, endpointsConfig], ); diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index a738ffb429..8925a37912 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -22,5 +22,4 @@ export { default as useScrollToRef } from './useScrollToRef'; export { default as useLocalStorage } from './useLocalStorage'; export { default as useDelayedRender } from './useDelayedRender'; export { default as useOnClickOutside } from './useOnClickOutside'; -export { default as useOriginNavigate } from './useOriginNavigate'; export { default as useGenerationsByLatest } from './useGenerationsByLatest'; diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index c345b9ed4b..6b47b545d5 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -4,6 +4,7 @@ import { useGetStartupConfig, useGetEndpointsQuery, } from 'librechat-data-provider/react-query'; +import { useNavigate } from 'react-router-dom'; import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider'; import { useRecoilState, @@ -30,12 +31,12 @@ import { } from '~/utils'; import useAssistantListMap from './Assistants/useAssistantListMap'; import { useDeleteFilesMutation } from '~/data-provider'; -import useOriginNavigate from './useOriginNavigate'; + import { mainTextareaId } from '~/common'; import store from '~/store'; const useNewConvo = (index = 0) => { - const navigate = useOriginNavigate(); + const navigate = useNavigate(); const { data: startupConfig } = useGetStartupConfig(); const defaultPreset = useRecoilValue(store.defaultPreset); const { setConversation } = store.useCreateConversationAtom(index); @@ -147,7 +148,7 @@ const useNewConvo = (index = 0) => { if (appTitle) { document.title = appTitle; } - navigate('new'); + navigate('/c/new'); } clearTimeout(timeoutIdRef.current); diff --git a/client/src/hooks/useOriginNavigate.ts b/client/src/hooks/useOriginNavigate.ts deleted file mode 100644 index 9c5ca68e50..0000000000 --- a/client/src/hooks/useOriginNavigate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useNavigate, useLocation } from 'react-router-dom'; - -const useOriginNavigate = () => { - const _navigate = useNavigate(); - const location = useLocation(); - - const navigate = (url?: string | null, opts = {}) => { - if (!url) { - return; - } - const path = location.pathname.match(/^\/[^/]+\//); - _navigate(`${path ? path[0] : '/c/'}${url}`, opts); - }; - - return navigate; -}; - -export default useOriginNavigate; diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 84216e08b6..00d6323856 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -20,6 +20,9 @@ export default { com_sidepanel_attach_files: 'Attach Files', com_sidepanel_manage_files: 'Manage Files', com_assistants_capabilities: 'Capabilities', + com_assistants_file_search: 'File Search', + com_assistants_file_search_info: + 'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.', com_assistants_knowledge: 'Knowledge', com_assistants_knowledge_info: 'If you upload files under Knowledge, conversations with your Assistant may include file contents.', @@ -35,6 +38,8 @@ export default { com_assistants_actions: 'Actions', com_assistants_add_tools: 'Add Tools', com_assistants_add_actions: 'Add Actions', + com_assistants_non_retrieval_model: + 'File search is not enabled on this model. Please select another model.', com_assistants_available_actions: 'Available Actions', com_assistants_running_action: 'Running action', com_assistants_completed_action: 'Talked to {0}', @@ -73,6 +78,8 @@ export default { com_ui_field_required: 'This field is required', com_ui_download_error: 'Error downloading file. The file may have been deleted.', com_ui_attach_error_type: 'Unsupported file type for endpoint:', + com_ui_attach_error_openai: 'Cannot attach Assistant files to other endpoints', + com_ui_attach_warn_endpoint: 'Non-Assistant files may be ignored without a compatible tool', com_ui_attach_error_size: 'File size limit exceeded for endpoint:', com_ui_attach_error: 'Cannot attach file. Create or select a conversation, or try refreshing the page.', diff --git a/client/src/mobile.css b/client/src/mobile.css index 50d84c1efe..80235fc107 100644 --- a/client/src/mobile.css +++ b/client/src/mobile.css @@ -270,4 +270,8 @@ .radix-side-top\:animate-slideDownAndFade[data-side=top] { -webkit-animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1); animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1) +} + +.azure-bg-color { + background: linear-gradient(0.375turn, #61bde2, #4389d0); } \ No newline at end of file diff --git a/librechat.example.yaml b/librechat.example.yaml index a2720b7fe7..2699d3146a 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -41,7 +41,7 @@ registration: endpoints: # assistants: # disableBuilder: false # Disable Assistants Builder Interface by setting to `true` - # pollIntervalMs: 750 # Polling interval for checking assistant updates + # pollIntervalMs: 3000 # Polling interval for checking assistant updates # timeoutMs: 180000 # Timeout for assistant operations # # Should only be one or the other, either `supportedIds` or `excludedIds` # supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"] diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index add6ed869a..8f638a7585 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.6.4", + "version": "0.6.5", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index fa2776fa72..7deccbed0b 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -683,7 +683,7 @@ export enum Constants { /** Key for the app's version. */ VERSION = 'v0.7.2', /** Key for the Custom Config's version (librechat.yaml). */ - CONFIG_VERSION = '1.1.0', + CONFIG_VERSION = '1.1.1', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ NO_PARENT = '00000000-0000-0000-0000-000000000000', /** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */ diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 5936acabc7..3554884184 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -334,9 +334,10 @@ export const getFileDownload = async (userId: string, file_id: string): Promise< export const deleteFiles = async ( files: f.BatchFile[], assistant_id?: string, + tool_resource?: a.EToolResources, ): Promise => request.deleteWithOptions(endpoints.files(), { - data: { files, assistant_id }, + data: { files, assistant_id, tool_resource }, }); /* actions */ diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 4f72756617..423e56b61e 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -16,6 +16,11 @@ export enum Tools { function = 'function', } +export enum EToolResources { + code_interpreter = 'code_interpreter', + file_search = 'file_search', +} + export type Tool = { [type: string]: Tools; }; @@ -94,6 +99,7 @@ export type AssistantUpdateParams = { metadata?: Metadata | null; name?: string | null; tools?: Array; + tool_resources?: ToolResources; endpoint: AssistantsEndpoint; }; diff --git a/packages/data-provider/src/types/files.ts b/packages/data-provider/src/types/files.ts index a5b5107030..3459b0782c 100644 --- a/packages/data-provider/src/types/files.ts +++ b/packages/data-provider/src/types/files.ts @@ -1,3 +1,5 @@ +import { EToolResources } from './assistants'; + export enum FileSources { local = 'local', firebase = 'firebase', @@ -7,6 +9,9 @@ export enum FileSources { vectordb = 'vectordb', } +export const checkOpenAIStorage = (source: string) => + source === FileSources.openai || source === FileSources.azure; + export enum FileContext { avatar = 'avatar', unknown = 'unknown', @@ -55,6 +60,7 @@ export type TFile = { usage: number; context?: FileContext; source?: FileSources; + filterSource?: FileSources; width?: number; height?: number; expiresAt?: string | Date; @@ -98,6 +104,7 @@ export type BatchFile = { export type DeleteFilesBody = { files: BatchFile[]; assistant_id?: string; + tool_resource?: EToolResources; }; export type DeleteMutationOptions = {