🤖 Assistants V2 Support: Part 2

🎹 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
This commit is contained in:
Danny Avila 2024-05-18 08:01:02 -04:00
parent 2bdbff5141
commit bc46ccdcad
No known key found for this signature in database
GPG key ID: 2DD9CC89B9B50364
44 changed files with 420 additions and 174 deletions

View file

@ -3,11 +3,11 @@ const {
Constants, Constants,
RunStatus, RunStatus,
CacheKeys, CacheKeys,
FileSources,
ContentTypes, ContentTypes,
EModelEndpoint, EModelEndpoint,
ViolationTypes, ViolationTypes,
ImageVisionTool, ImageVisionTool,
checkOpenAIStorage,
AssistantStreamEvents, AssistantStreamEvents,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { const {
@ -361,10 +361,7 @@ const chatV2 = async (req, res) => {
/** @type {MongoFile[]} */ /** @type {MongoFile[]} */
const attachments = await req.body.endpointOption.attachments; const attachments = await req.body.endpointOption.attachments;
if ( if (attachments && attachments.every((attachment) => checkOpenAIStorage(attachment.source))) {
attachments &&
attachments.every((attachment) => attachment.source === FileSources.openai)
) {
return; return;
} }
@ -422,7 +419,7 @@ const chatV2 = async (req, res) => {
if (processedFiles) { if (processedFiles) {
for (const file of processedFiles) { for (const file of processedFiles) {
if (file.source !== FileSources.openai) { if (!checkOpenAIStorage(file.source)) {
attachedFileIds.delete(file.file_id); attachedFileIds.delete(file.file_id);
const index = file_ids.indexOf(file.file_id); const index = file_ids.indexOf(file.file_id);
if (index > -1) { if (index > -1) {

View file

@ -1,9 +1,4 @@
const { const { EModelEndpoint, CacheKeys, defaultAssistantsVersion } = require('librechat-data-provider');
EModelEndpoint,
FileSources,
CacheKeys,
defaultAssistantsVersion,
} = require('librechat-data-provider');
const { const {
initializeClient: initAzureClient, initializeClient: initAzureClient,
} = require('~/server/services/Endpoints/azureAssistants'); } = require('~/server/services/Endpoints/azureAssistants');
@ -121,13 +116,8 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que
}; };
}; };
async function getOpenAIClient({ req, res, endpointOption, initAppClient }) { async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) {
let endpoint = req.body.endpoint ?? req.query.endpoint; let endpoint = overrideEndpoint ?? 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;
}
const version = await getCurrentVersion(req, endpoint); const version = await getCurrentVersion(req, endpoint);
if (!endpoint) { if (!endpoint) {
throw new Error(`[${req.baseUrl}] Endpoint is required`); throw new Error(`[${req.baseUrl}] Endpoint is required`);

View file

@ -1,3 +1,4 @@
const { ToolCallTypes } = require('librechat-data-provider');
const { validateAndUpdateTool } = require('~/server/services/ActionService'); const { validateAndUpdateTool } = require('~/server/services/ActionService');
const { getOpenAIClient } = require('./helpers'); const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config'); const { logger } = require('~/config');
@ -54,6 +55,7 @@ const createAssistant = async (req, res) => {
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
const tools = []; const tools = [];
let hasFileSearch = false;
for (const tool of updateData.tools ?? []) { for (const tool of updateData.tools ?? []) {
let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool; let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool;
@ -61,6 +63,10 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
continue; continue;
} }
if (actualTool.type === ToolCallTypes.FILE_SEARCH) {
hasFileSearch = true;
}
if (!actualTool.function) { if (!actualTool.function) {
tools.push(actualTool); tools.push(actualTool);
continue; 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; updateData.tools = tools;
if (openai.locals?.azureOptions && updateData.model) { if (openai.locals?.azureOptions && updateData.model) {

View file

@ -1,6 +1,6 @@
const fs = require('fs').promises; const fs = require('fs').promises;
const express = require('express'); const express = require('express');
const { isUUID, FileSources } = require('librechat-data-provider'); const { isUUID, checkOpenAIStorage } = require('librechat-data-provider');
const { const {
filterFile, filterFile,
processFileUpload, processFileUpload,
@ -89,7 +89,7 @@ router.get('/download/:userId/:file_id', async (req, res) => {
return res.status(403).send('Forbidden'); 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}`); logger.warn(`${errorPrefix} has no associated model: ${file_id}`);
return res.status(400).send('The model used when creating this file is not available'); 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; let passThrough;
/** @type {ReadableStream | undefined} */ /** @type {ReadableStream | undefined} */
let fileStream; let fileStream;
if (file.source === FileSources.openai) {
if (checkOpenAIStorage(file.source)) {
req.body = { model: file.model }; req.body = { model: file.model };
const { openai } = await initializeClient({ req, res }); const { openai } = await initializeClient({ req, res });
logger.debug(`Downloading file ${file_id} from OpenAI`); logger.debug(`Downloading file ${file_id} from OpenAI`);

View file

@ -78,7 +78,7 @@ async function createOnTextProgress({
* @return {Promise<OpenAIAssistantFinish | OpenAIAssistantAction[] | ThreadMessage[] | RequiredActionFunctionToolCall[]>} * @return {Promise<OpenAIAssistantFinish | OpenAIAssistantAction[] | ThreadMessage[] | RequiredActionFunctionToolCall[]>}
*/ */
async function getResponse({ openai, run_id, thread_id }) { 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) { if (run.status === RunStatus.COMPLETED) {
const messages = await openai.beta.threads.messages.list(thread_id, defaultOrderQuery); const messages = await openai.beta.threads.messages.list(thread_id, defaultOrderQuery);

View file

@ -15,6 +15,44 @@ const OpenAIClient = require('~/app/clients/OpenAIClient');
const { isUserProvided } = require('~/server/utils'); const { isUserProvided } = require('~/server/utils');
const { constructAzureURL } = require('~/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 initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => {
const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } = const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } =
process.env; process.env;
@ -130,6 +168,8 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
...opts, ...opts,
}); });
openai.beta.assistants.files = new Files(openai);
openai.req = req; openai.req = req;
openai.res = res; openai.res = res;

View file

@ -180,7 +180,15 @@ const deleteFirebaseFile = async (req, file) => {
if (!fileName.includes(req.user.id)) { if (!fileName.includes(req.user.id)) {
throw new Error('Invalid file path'); throw new Error('Invalid file path');
} }
try {
await deleteFile('', fileName); await deleteFile('', fileName);
} catch (error) {
logger.error('Error deleting file from Firebase:', error);
if (error.code === 'storage/object-not-found') {
return;
}
throw error;
}
}; };
/** /**

View file

@ -10,21 +10,19 @@ const {
EModelEndpoint, EModelEndpoint,
mergeFileConfig, mergeFileConfig,
hostImageIdSuffix, hostImageIdSuffix,
checkOpenAIStorage,
hostImageNamePrefix, hostImageNamePrefix,
isAssistantsEndpoint, isAssistantsEndpoint,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images'); const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
const { addResourceFileId } = require('~/server/controllers/assistants/v2');
const { LB_QueueAsyncCall } = require('~/server/utils/queue'); const { LB_QueueAsyncCall } = require('~/server/utils/queue');
const { getStrategyFunctions } = require('./strategies'); const { getStrategyFunctions } = require('./strategies');
const { determineFileType } = require('~/server/utils'); const { determineFileType } = require('~/server/utils');
const { logger } = require('~/config'); const { logger } = require('~/config');
const checkOpenAIStorage = (source) =>
source === FileSources.openai || source === FileSources.azure;
const processFiles = async (files) => { const processFiles = async (files) => {
const promises = []; const promises = [];
for (let file of files) { 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. * 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 {object} params - The passed parameters.
* @param {MongoFile} file - The file object to delete. * @param {Express.Request} params.req - The express request object.
* @param {Function} deleteFile - The delete file function. * @param {MongoFile} params.file - The file object to delete.
* @param {Promise[]} promises - The array of promises to await. * @param {Function} params.deleteFile - The delete file function.
* @param {OpenAI | undefined} [openai] - If an OpenAI file, the initialized OpenAI client. * @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)) { if (checkOpenAIStorage(file.source)) {
// Enqueue to leaky bucket // Enqueue to leaky bucket
promises.push( promises.push(
@ -58,6 +58,7 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
logger.error('Error deleting file from OpenAI source', err); logger.error('Error deleting file from OpenAI source', err);
reject(err); reject(err);
} else { } else {
resolvedFileIds.push(file.file_id);
resolve(result); resolve(result);
} }
}, },
@ -67,7 +68,9 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
} else { } else {
// Add directly to promises // Add directly to promises
promises.push( promises.push(
deleteFile(req, file).catch((err) => { deleteFile(req, file)
.then(() => resolvedFileIds.push(file.file_id))
.catch((err) => {
logger.error('Error deleting file', err); logger.error('Error deleting file', err);
return Promise.reject(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 {Express.Request} params.req - The express request object.
* @param {DeleteFilesBody} params.req.body - The request body. * @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.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<void>} * @returns {Promise<void>}
*/ */
const processDeleteRequest = async ({ req, files }) => { const processDeleteRequest = async ({ req, files }) => {
const file_ids = files.map((file) => file.file_id); const resolvedFileIds = [];
const deletionMethods = {}; const deletionMethods = {};
const promises = []; const promises = [];
promises.push(deleteFiles(file_ids));
/** @type {OpenAI | undefined} */ /** @type {Record<string, OpenAI | undefined>} */
let openai; const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined };
if (req.body.assistant_id) { const initializeClients = async () => {
({ openai } = await getOpenAIClient({ req })); 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) { for (const file of files) {
const source = file.source ?? FileSources.local; const source = file.source ?? FileSources.local;
if (checkOpenAIStorage(source) && !openai) { if (checkOpenAIStorage(source) && !client[source]) {
({ openai } = await getOpenAIClient({ req })); 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)); promises.push(openai.beta.assistants.files.del(req.body.assistant_id, file.file_id));
} }
if (deletionMethods[source]) { if (deletionMethods[source]) {
enqueueDeleteOperation(req, file, deletionMethods[source], promises, openai); enqueueDeleteOperation({
req,
file,
deleteFile: deletionMethods[source],
promises,
resolvedFileIds,
openai,
});
continue; continue;
} }
@ -123,10 +162,11 @@ const processDeleteRequest = async ({ req, files }) => {
} }
deletionMethods[source] = deleteFile; deletionMethods[source] = deleteFile;
enqueueDeleteOperation(req, file, deleteFile, promises, openai); enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai });
} }
await Promise.allSettled(promises); await Promise.allSettled(promises);
await deleteFiles(resolvedFileIds);
}; };
/** /**
@ -381,7 +421,10 @@ const processOpenAIFile = async ({
originalName ? `/${originalName}` : '' originalName ? `/${originalName}` : ''
}`; }`;
const type = mime.getType(originalName ?? file_id); const type = mime.getType(originalName ?? file_id);
const source =
openai.req.body.endpoint === EModelEndpoint.azureAssistants
? FileSources.azure
: FileSources.openai;
const file = { const file = {
..._file, ..._file,
type, type,
@ -390,7 +433,7 @@ const processOpenAIFile = async ({
usage: 1, usage: 1,
user: userId, user: userId,
context: _file.purpose, context: _file.purpose,
source: FileSources.openai, source,
model: openai.req.body.model, model: openai.req.body.model,
filename: originalName ?? file_id, filename: originalName ?? file_id,
}; };
@ -435,12 +478,14 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx
filename: `${hostImageNamePrefix}${filename}`, filename: `${hostImageNamePrefix}${filename}`,
}; };
createFile(file, true); createFile(file, true);
const source =
req.body.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai;
createFile( createFile(
{ {
...file, ...file,
file_id, file_id,
filename, filename,
source: FileSources.openai, source,
type: mime.getType(fileExt), type: mime.getType(fileExt),
}, },
true, true,

View file

@ -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.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 {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 {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). * @param {number} [params.timeout=180000] - The period to wait until timing out polling; default is 3 minutes (in ms).
* @return {Promise<Run>} A promise that resolves to the last fetched run object. * @return {Promise<Run>} A promise that resolves to the last fetched run object.
*/ */
@ -64,7 +64,7 @@ async function waitForRun({
run_id, run_id,
thread_id, thread_id,
runManager, runManager,
pollIntervalMs = 750, pollIntervalMs = 2000,
timeout = 60000 * 3, timeout = 60000 * 3,
}) { }) {
let timeElapsed = 0; let timeElapsed = 0;
@ -233,7 +233,7 @@ async function _handleRun({ openai, run_id, thread_id }) {
run_id, run_id,
thread_id, thread_id,
runManager, runManager,
pollIntervalMs: 750, pollIntervalMs: 2000,
timeout: 60000, timeout: 60000,
}); });
const actions = []; const actions = [];

View file

@ -326,6 +326,7 @@ export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
iconURL?: string; iconURL?: string;
message?: boolean; message?: boolean;
className?: string; className?: string;
iconClassName?: string;
endpoint?: EModelEndpoint | string | null; endpoint?: EModelEndpoint | string | null;
endpointType?: EModelEndpoint | null; endpointType?: EModelEndpoint | null;
assistantName?: string; assistantName?: string;

View file

@ -2,6 +2,7 @@ import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import FileIcon from '~/components/svg/Files/FileIcon'; import FileIcon from '~/components/svg/Files/FileIcon';
import ProgressCircle from './ProgressCircle'; import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { useProgress } from '~/hooks'; import { useProgress } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -21,7 +22,6 @@ const FilePreview = ({
const radius = 55; // Radius of the SVG circle const radius = 55; // Radius of the SVG circle
const circumference = 2 * Math.PI * radius; const circumference = 2 * Math.PI * radius;
const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1); const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1);
console.log(progress);
// Calculate the offset based on the loading progress // Calculate the offset based on the loading progress
const offset = circumference - progress * circumference; const offset = circumference - progress * circumference;
@ -32,6 +32,7 @@ const FilePreview = ({
return ( return (
<div className={cn('h-10 w-10 shrink-0 overflow-hidden rounded-md', className)}> <div className={cn('h-10 w-10 shrink-0 overflow-hidden rounded-md', className)}>
<FileIcon file={file} fileType={fileType} /> <FileIcon file={file} fileType={fileType} />
<SourceIcon source={file?.source} />
{progress < 1 && ( {progress < 1 && (
<ProgressCircle <ProgressCircle
circumference={circumference} circumference={circumference}

View file

@ -1,4 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { EToolResources } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider'; import { useDeleteFilesMutation } from '~/data-provider';
import { useFileDeletion } from '~/hooks/Files'; import { useFileDeletion } from '~/hooks/Files';
@ -10,6 +11,7 @@ export default function FileRow({
setFiles, setFiles,
setFilesLoading, setFilesLoading,
assistant_id, assistant_id,
tool_resource,
fileFilter, fileFilter,
Wrapper, Wrapper,
}: { }: {
@ -18,6 +20,7 @@ export default function FileRow({
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>; setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
fileFilter?: (file: ExtendedFile) => boolean; fileFilter?: (file: ExtendedFile) => boolean;
assistant_id?: string; assistant_id?: string;
tool_resource?: EToolResources;
Wrapper?: React.FC<{ children: React.ReactNode }>; Wrapper?: React.FC<{ children: React.ReactNode }>;
}) { }) {
const files = Array.from(_files.values()).filter((file) => const files = Array.from(_files.values()).filter((file) =>
@ -25,7 +28,8 @@ export default function FileRow({
); );
const { mutateAsync } = useDeleteFilesMutation({ 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: () => { onSuccess: () => {
console.log('Files deleted'); 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(() => { useEffect(() => {
if (!files) { if (!files) {
@ -82,6 +86,7 @@ export default function FileRow({
url={file.preview} url={file.preview}
onDelete={handleDelete} onDelete={handleDelete}
progress={file.progress} progress={file.progress}
source={file.source}
/> />
); );
} }

View file

@ -12,16 +12,9 @@ export default function Files({ open, onOpenChange }) {
const { data: files = [] } = useGetFiles<TFile[]>({ const { data: files = [] } = useGetFiles<TFile[]>({
select: (files) => select: (files) =>
files.map((file) => { files.map((file) => {
if (file.source === FileSources.local || file.source === FileSources.openai) {
file.context = file.context ?? FileContext.unknown; file.context = file.context ?? FileContext.unknown;
file.filterSource = file.source === FileSources.firebase ? FileSources.local : file.source;
return file; return file;
} else {
return {
...file,
context: file.context ?? FileContext.unknown,
source: FileSources.local,
};
}
}), }),
}); });

View file

@ -1,3 +1,4 @@
import { FileSources } from 'librechat-data-provider';
import ImagePreview from './ImagePreview'; import ImagePreview from './ImagePreview';
import RemoveFile from './RemoveFile'; import RemoveFile from './RemoveFile';
@ -6,16 +7,18 @@ const Image = ({
url, url,
onDelete, onDelete,
progress = 1, progress = 1,
source = FileSources.local,
}: { }: {
imageBase64?: string; imageBase64?: string;
url?: string; url?: string;
onDelete: () => void; onDelete: () => void;
progress: number; // between 0 and 1 progress: number; // between 0 and 1
source?: FileSources;
}) => { }) => {
return ( return (
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90"> <div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600"> <div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
<ImagePreview imageBase64={imageBase64} url={url} progress={progress} /> <ImagePreview source={source} imageBase64={imageBase64} url={url} progress={progress} />
</div> </div>
<RemoveFile onRemove={onDelete} /> <RemoveFile onRemove={onDelete} />
</div> </div>

View file

@ -1,4 +1,6 @@
import { FileSources } from 'librechat-data-provider';
import ProgressCircle from './ProgressCircle'; import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { cn } from '~/utils'; import { cn } from '~/utils';
type styleProps = { type styleProps = {
@ -13,11 +15,13 @@ const ImagePreview = ({
url, url,
progress = 1, progress = 1,
className = '', className = '',
source,
}: { }: {
imageBase64?: string; imageBase64?: string;
url?: string; url?: string;
progress?: number; // between 0 and 1 progress?: number; // between 0 and 1
className?: string; className?: string;
source?: FileSources;
}) => { }) => {
let style: styleProps = { let style: styleProps = {
backgroundSize: 'cover', backgroundSize: 'cover',
@ -65,6 +69,7 @@ const ImagePreview = ({
circleCSSProperties={circleCSSProperties} circleCSSProperties={circleCSSProperties}
/> />
)} )}
<SourceIcon source={source} />
</div> </div>
); );
}; };

View file

@ -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 (
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
<span className="flex items-center justify-center">
<MinimalIcon
endpoint={endpoint}
size={14}
isCreatedByUser={false}
iconClassName="h-3 w-3"
/>
</span>
</button>
);
}

View file

@ -7,6 +7,7 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
import FilePreview from '~/components/Chat/Input/Files/FilePreview'; import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { SortFilterHeader } from './SortFilterHeader'; import { SortFilterHeader } from './SortFilterHeader';
import { OpenAIMinimalIcon } from '~/components/svg'; import { OpenAIMinimalIcon } from '~/components/svg';
import { AzureMinimalIcon } from '~/components/svg';
import { Button, Checkbox } from '~/components/ui'; import { Button, Checkbox } from '~/components/ui';
import { formatDate, getFileType } from '~/utils'; import { formatDate, getFileType } from '~/utils';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
@ -74,7 +75,8 @@ export const columns: ColumnDef<TFile>[] = [
<div className="flex gap-2"> <div className="flex gap-2">
<ImagePreview <ImagePreview
url={file.filepath} url={file.filepath}
className="h-10 w-10 shrink-0 overflow-hidden rounded-md" className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file?.source}
/> />
<span className="self-center truncate ">{file.filename}</span> <span className="self-center truncate ">{file.filename}</span>
</div> </div>
@ -84,7 +86,7 @@ export const columns: ColumnDef<TFile>[] = [
const fileType = getFileType(file.type); const fileType = getFileType(file.type);
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
{fileType && <FilePreview fileType={fileType} />} {fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span> <span className="self-center truncate">{file.filename}</span>
</div> </div>
); );
@ -108,7 +110,7 @@ export const columns: ColumnDef<TFile>[] = [
cell: ({ row }) => formatDate(row.original.updatedAt), cell: ({ row }) => formatDate(row.original.updatedAt),
}, },
{ {
accessorKey: 'source', accessorKey: 'filterSource',
header: ({ column }) => { header: ({ column }) => {
const localize = useLocalize(); const localize = useLocalize();
return ( return (
@ -117,10 +119,14 @@ export const columns: ColumnDef<TFile>[] = [
title={localize('com_ui_storage')} title={localize('com_ui_storage')}
filters={{ filters={{
Storage: Object.values(FileSources).filter( Storage: Object.values(FileSources).filter(
(value) => value === FileSources.local || value === FileSources.openai, (value) =>
value === FileSources.local ||
value === FileSources.openai ||
value === FileSources.azure,
), ),
}} }}
valueMap={{ valueMap={{
[FileSources.azure]: 'Azure',
[FileSources.openai]: 'OpenAI', [FileSources.openai]: 'OpenAI',
[FileSources.local]: 'com_ui_host', [FileSources.local]: 'com_ui_host',
}} }}
@ -137,6 +143,13 @@ export const columns: ColumnDef<TFile>[] = [
{'OpenAI'} {'OpenAI'}
</div> </div>
); );
} else if (source === FileSources.azure) {
return (
<div className="flex flex-wrap items-center gap-2">
<AzureMinimalIcon className="icon-sm text-cyan-700" />
{'Azure'}
</div>
);
} }
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">

View file

@ -48,7 +48,12 @@ const contextMap = {
[FileContext.bytes]: 'com_ui_size', [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<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) { export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const localize = useLocalize(); const localize = useLocalize();
@ -142,7 +147,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => { {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') { if (header.id === 'filename') {
style.maxWidth = '50%'; style.maxWidth = '50%';
style.width = '50%'; style.width = '50%';

View file

@ -15,7 +15,7 @@ import { cn } from '~/utils';
import { IconProps } from '~/common'; import { IconProps } from '~/common';
const MinimalIcon: React.FC<IconProps> = (props) => { const MinimalIcon: React.FC<IconProps> = (props) => {
const { size = 30, error } = props; const { size = 30, iconClassName, error } = props;
let endpoint = 'default'; // Default value for endpoint let endpoint = 'default'; // Default value for endpoint
@ -25,10 +25,13 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
const endpointIcons = { const endpointIcons = {
[EModelEndpoint.azureOpenAI]: { [EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon />, icon: <AzureMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
},
[EModelEndpoint.openAI]: {
icon: <OpenAIMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT', name: props.chatGptLabel || 'ChatGPT',
}, },
[EModelEndpoint.openAI]: { icon: <OpenAIMinimalIcon />, name: props.chatGptLabel || 'ChatGPT' },
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' }, [EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' }, [EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' },
[EModelEndpoint.anthropic]: { [EModelEndpoint.anthropic]: {

View file

@ -71,6 +71,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
}} }}
placeholder={localize('com_nav_search_placeholder')} placeholder={localize('com_nav_search_placeholder')}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
autoComplete="off"
/> />
<X <X
className={cn( className={cn(

View file

@ -134,7 +134,7 @@ const ApiKey = () => {
<input <input
placeholder="<HIDDEN>" placeholder="<HIDDEN>"
type="password" type="password"
autoComplete="off" autoComplete="new-password"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600" className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })} {...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
/> />

View file

@ -4,6 +4,7 @@ import {
Tools, Tools,
FileSources, FileSources,
Capabilities, Capabilities,
EModelEndpoint,
LocalStorageKeys, LocalStorageKeys,
isImageVisionTool, isImageVisionTool,
defaultAssistantFormValues, defaultAssistantFormValues,
@ -52,6 +53,8 @@ export default function AssistantSelect({
const assistants = useListAssistantsQuery(endpoint, undefined, { const assistants = useListAssistantsQuery(endpoint, undefined, {
select: (res) => select: (res) =>
res.data.map((_assistant) => { res.data.map((_assistant) => {
const source =
endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure;
const assistant = { const assistant = {
..._assistant, ..._assistant,
label: _assistant?.name ?? '', label: _assistant?.name ?? '',
@ -77,7 +80,7 @@ export default function AssistantSelect({
size: file.bytes, size: file.bytes,
preview: file.filepath, preview: file.filepath,
progress: 1, progress: 1,
source: FileSources.openai, source,
}, },
]); ]);
} else { } else {
@ -90,7 +93,7 @@ export default function AssistantSelect({
size: 1, size: 1,
progress: 1, progress: 1,
filepath: endpoint, filepath: endpoint,
source: FileSources.openai, source,
}, },
]); ]);
} }

View file

@ -63,7 +63,6 @@ export default function Code({
version={version} version={version}
endpoint={endpoint} endpoint={endpoint}
files={files} files={files}
tool_resource={Capabilities.code_interpreter}
/> />
)} )}
</> </>

View file

@ -1,5 +1,9 @@
import { useState, useRef, useEffect } from 'react'; 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 { AssistantsEndpoint } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow'; import FileRow from '~/components/Chat/Input/Files/FileRow';
@ -8,17 +12,17 @@ import { useFileHandling } from '~/hooks/Files';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
export default function Knowledge({ const tool_resource = EToolResources.code_interpreter;
export default function CodeFiles({
endpoint, endpoint,
assistant_id, assistant_id,
files: _files, files: _files,
tool_resource,
}: { }: {
version: number | string; version: number | string;
endpoint: AssistantsEndpoint; endpoint: AssistantsEndpoint;
assistant_id: string; assistant_id: string;
files?: [string, ExtendedFile][]; files?: [string, ExtendedFile][];
tool_resource?: string;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const { setFilesLoading } = useChatContext(); const { setFilesLoading } = useChatContext();
@ -57,13 +61,14 @@ export default function Knowledge({
<div className={'mb-2'}> <div className={'mb-2'}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs"> <div className="text-token-text-tertiary rounded-lg text-xs">
{assistant_id ? localize('com_assistants_knowledge_info') : ''} {localize('com_assistants_code_interpreter_files')}
</div> </div>
<FileRow <FileRow
files={files} files={files}
setFiles={setFiles} setFiles={setFiles}
setFilesLoading={setFilesLoading}
assistant_id={assistant_id} assistant_id={assistant_id}
tool_resource={tool_resource}
setFilesLoading={setFilesLoading}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>} Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/> />
<div> <div>

View file

@ -41,10 +41,10 @@ export const AssistantAvatar = ({
return ( return (
<div> <div>
<div className="relative overflow-hidden rounded-full"> <div className="relative h-20 w-20 overflow-hidden rounded-full">
<img <img
src={url} src={url}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full" className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt="GPT" alt="GPT"
width="80" width="80"
height="80" height="80"

View file

@ -3,8 +3,10 @@ import { useFormContext, Controller, useWatch } from 'react-hook-form';
import { Capabilities } from 'librechat-data-provider'; import { Capabilities } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider'; import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common'; import type { AssistantForm } from '~/common';
import { Checkbox } from '~/components/ui'; import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
import { cn } from '~/utils/'; import { cn } from '~/utils/';
export default function Retrieval({ export default function Retrieval({
@ -21,20 +23,23 @@ export default function Retrieval({
const model = useWatch({ control, name: 'model' }); const model = useWatch({ control, name: 'model' });
const assistant = useWatch({ control, name: 'assistant' }); const assistant = useWatch({ control, name: 'assistant' });
const files = useMemo(() => { const vectorStores = useMemo(() => {
if (typeof assistant === 'string') { if (typeof assistant === 'string') {
return []; return [];
} }
return assistant.tool_resources?.file_search; return assistant.tool_resources?.file_search;
}, [assistant]); }, [assistant]);
const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]);
useEffect(() => { useEffect(() => {
if (model && !retrievalModels.has(model)) { if (model && isDisabled) {
setValue(Capabilities.retrieval, false); setValue(Capabilities.retrieval, false);
} }
}, [model, setValue, retrievalModels]); }, [model, setValue, isDisabled]);
return ( return (
<>
<div className="flex items-center"> <div className="flex items-center">
<Controller <Controller
name={Capabilities.retrieval} name={Capabilities.retrieval}
@ -43,17 +48,19 @@ export default function Retrieval({
<Checkbox <Checkbox
{...field} {...field}
checked={field.value} checked={field.value}
disabled={!retrievalModels.has(model)} disabled={isDisabled}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()} value={field?.value?.toString()}
/> />
)} )}
/> />
<HoverCard openDelay={50}>
<HoverCardTrigger asChild>
<label <label
className={cn( className={cn(
'form-check-label text-token-text-primary w-full', 'form-check-label text-token-text-primary w-full select-none',
!retrievalModels.has(model) ? 'cursor-no-drop opacity-50' : 'cursor-pointer', isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
)} )}
htmlFor={Capabilities.retrieval} htmlFor={Capabilities.retrieval}
onClick={() => onClick={() =>
@ -63,8 +70,25 @@ export default function Retrieval({
}) })
} }
> >
{localize('com_assistants_retrieval')} {version == 1
? localize('com_assistants_retrieval')
: localize('com_assistants_file_search')}
</label> </label>
</HoverCardTrigger>
<OptionHover
side={ESide.Top}
disabled={!isDisabled}
description="com_assistants_non_retrieval_model"
langCode={true}
sideOffset={20}
/>
</HoverCard>
</div> </div>
{version == 2 && (
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_file_search_info')}
</div>
)}
</>
); );
} }

View file

@ -1,8 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { import {
fileConfig as defaultFileConfig, fileConfig as defaultFileConfig,
checkOpenAIStorage,
mergeFileConfig, mergeFileConfig,
megabyte, megabyte,
isAssistantsEndpoint,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { Row } from '@tanstack/react-table'; import type { Row } from '@tanstack/react-table';
import type { TFile } from 'librechat-data-provider'; import type { TFile } from 'librechat-data-provider';
@ -36,6 +38,18 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
return showToast({ message: localize('com_ui_attach_error'), status: 'error' }); 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 } = const { fileSizeLimit, supportedMimeTypes } =
fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default; fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default;
@ -81,7 +95,8 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
> >
<ImagePreview <ImagePreview
url={file.filepath} url={file.filepath}
className="h-10 w-10 shrink-0 overflow-hidden rounded-md" className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file.source}
/> />
<span className="self-center truncate text-xs">{file.filename}</span> <span className="self-center truncate text-xs">{file.filename}</span>
</div> </div>
@ -94,7 +109,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
onClick={handleFileClick} onClick={handleFileClick}
className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700" className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700"
> >
{fileType && <FilePreview fileType={fileType} />} {fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span> <span className="self-center truncate">{file.filename}</span>
</div> </div>
); );

View file

@ -7,11 +7,21 @@ type TOptionHoverProps = {
description: string; description: string;
langCode?: boolean; langCode?: boolean;
sideOffset?: number; sideOffset?: number;
disabled?: boolean;
side: ESide; side: ESide;
}; };
function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) { function OptionHover({
side,
description,
disabled,
langCode,
sideOffset = 30,
}: TOptionHoverProps) {
const localize = useLocalize(); const localize = useLocalize();
if (disabled) {
return null;
}
const text = langCode ? localize(description) : description; const text = langCode ? localize(description) : description;
return ( return (
<HoverCardPortal> <HoverCardPortal>

View file

@ -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 { useMutation, useQueryClient } from '@tanstack/react-query';
import type { UseMutationResult } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query';
import type t from 'librechat-data-provider'; import type t from 'librechat-data-provider';
@ -420,7 +424,7 @@ export const useUploadFileMutation = (
if (!tool_resource) { if (!tool_resource) {
update['file_ids'] = [...assistant.file_ids, data.file_id]; 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 prevResources = assistant.tool_resources ?? {};
const prevResource = assistant.tool_resources?.[tool_resource as string] ?? { const prevResource = assistant.tool_resources?.[tool_resource as string] ?? {
file_ids: [], file_ids: [],
@ -455,7 +459,8 @@ export const useDeleteFilesMutation = (
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { onSuccess, ...options } = _options || {}; const { onSuccess, ...options } = _options || {};
return useMutation([MutationKeys.fileDelete], { 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 || {}), ...(options || {}),
onSuccess: (data, ...args) => { onSuccess: (data, ...args) => {
queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => { queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {

View file

@ -1,4 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { import type {
@ -10,11 +11,10 @@ import type {
TEndpointsConfig, TEndpointsConfig,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store'; import store from '~/store';
const useConversation = () => { const useConversation = () => {
const navigate = useOriginNavigate(); const navigate = useNavigate();
const setConversation = useSetRecoilState(store.conversation); const setConversation = useSetRecoilState(store.conversation);
const resetLatestMessage = useResetRecoilState(store.latestMessage); const resetLatestMessage = useResetRecoilState(store.latestMessage);
const setMessages = useSetRecoilState<TMessagesAtom>(store.messages); const setMessages = useSetRecoilState<TMessagesAtom>(store.messages);
@ -59,7 +59,7 @@ const useConversation = () => {
resetLatestMessage(); resetLatestMessage();
if (conversation.conversationId === 'new' && !modelsData) { if (conversation.conversationId === 'new' && !modelsData) {
navigate('new'); navigate('/c/new');
} }
}, },
[endpointsConfig, modelsQuery.data], [endpointsConfig, modelsQuery.data],

View file

@ -1,14 +1,14 @@
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useSetRecoilState, useResetRecoilState } from 'recoil'; import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider'; import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store'; import store from '~/store';
const useNavigateToConvo = (index = 0) => { const useNavigateToConvo = (index = 0) => {
const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useOriginNavigate();
const { setConversation } = store.useCreateConversationAtom(index); const { setConversation } = store.useCreateConversationAtom(index);
const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index)); const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
@ -48,7 +48,7 @@ const useNavigateToConvo = (index = 0) => {
}); });
} }
setConversation(convo); setConversation(convo);
navigate(convo?.conversationId); navigate(`/c/${convo.conversationId ?? 'new'}`);
}; };
const navigateWithLastTools = (conversation: TConversation) => { const navigateWithLastTools = (conversation: TConversation) => {

View file

@ -1,5 +1,5 @@
import debounce from 'lodash/debounce'; 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 { useCallback, useState, useEffect } from 'react';
import type { import type {
BatchFile, BatchFile,
@ -16,18 +16,20 @@ type FileMapSetter = GenericSetter<Map<string, ExtendedFile>>;
const useFileDeletion = ({ const useFileDeletion = ({
mutateAsync, mutateAsync,
assistant_id, assistant_id,
tool_resource,
}: { }: {
mutateAsync: UseMutateAsyncFunction<DeleteFilesResponse, unknown, DeleteFilesBody, unknown>; mutateAsync: UseMutateAsyncFunction<DeleteFilesResponse, unknown, DeleteFilesBody, unknown>;
assistant_id?: string; assistant_id?: string;
tool_resource?: EToolResources;
}) => { }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_batch, setFileDeleteBatch] = useState<BatchFile[]>([]); const [_batch, setFileDeleteBatch] = useState<BatchFile[]>([]);
const setFilesToDelete = useSetFilesToDelete(); const setFilesToDelete = useSetFilesToDelete();
const executeBatchDelete = useCallback( const executeBatchDelete = useCallback(
(filesToDelete: BatchFile[], assistant_id?: string) => { (filesToDelete: BatchFile[], assistant_id?: string, tool_resource?: EToolResources) => {
console.log('Deleting files:', filesToDelete, assistant_id); console.log('Deleting files:', filesToDelete, assistant_id, tool_resource);
mutateAsync({ files: filesToDelete, assistant_id }); mutateAsync({ files: filesToDelete, assistant_id, tool_resource });
setFileDeleteBatch([]); setFileDeleteBatch([]);
}, },
[mutateAsync], [mutateAsync],
@ -81,11 +83,11 @@ const useFileDeletion = ({
setFileDeleteBatch((prevBatch) => { setFileDeleteBatch((prevBatch) => {
const newBatch = [...prevBatch, file]; const newBatch = [...prevBatch, file];
debouncedDelete(newBatch, assistant_id); debouncedDelete(newBatch, assistant_id, tool_resource);
return newBatch; return newBatch;
}); });
}, },
[debouncedDelete, setFilesToDelete, assistant_id], [debouncedDelete, setFilesToDelete, assistant_id, tool_resource],
); );
const deleteFiles = useCallback( const deleteFiles = useCallback(

View file

@ -58,23 +58,23 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
const assistantListMap = useMemo( const assistantListMap = useMemo(
() => ({ () => ({
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants] [EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
.map( ?.map(
assistantMapFn({ assistantMapFn({
endpoint: EModelEndpoint.assistants, endpoint: EModelEndpoint.assistants,
assistantMap, assistantMap,
endpointsConfig, endpointsConfig,
}), }),
) )
.filter(Boolean), ?.filter(Boolean),
[EModelEndpoint.azureAssistants]: listMap[EModelEndpoint.azureAssistants] [EModelEndpoint.azureAssistants]: listMap[EModelEndpoint.azureAssistants]
.map( ?.map(
assistantMapFn({ assistantMapFn({
endpoint: EModelEndpoint.azureAssistants, endpoint: EModelEndpoint.azureAssistants,
assistantMap, assistantMap,
endpointsConfig, endpointsConfig,
}), }),
) )
.filter(Boolean), ?.filter(Boolean),
}), }),
[listMap, assistantMap, endpointsConfig], [listMap, assistantMap, endpointsConfig],
); );

View file

@ -22,5 +22,4 @@ export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage'; export { default as useLocalStorage } from './useLocalStorage';
export { default as useDelayedRender } from './useDelayedRender'; export { default as useDelayedRender } from './useDelayedRender';
export { default as useOnClickOutside } from './useOnClickOutside'; export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useOriginNavigate } from './useOriginNavigate';
export { default as useGenerationsByLatest } from './useGenerationsByLatest'; export { default as useGenerationsByLatest } from './useGenerationsByLatest';

View file

@ -4,6 +4,7 @@ import {
useGetStartupConfig, useGetStartupConfig,
useGetEndpointsQuery, useGetEndpointsQuery,
} from 'librechat-data-provider/react-query'; } from 'librechat-data-provider/react-query';
import { useNavigate } from 'react-router-dom';
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider'; import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
import { import {
useRecoilState, useRecoilState,
@ -30,12 +31,12 @@ import {
} from '~/utils'; } from '~/utils';
import useAssistantListMap from './Assistants/useAssistantListMap'; import useAssistantListMap from './Assistants/useAssistantListMap';
import { useDeleteFilesMutation } from '~/data-provider'; import { useDeleteFilesMutation } from '~/data-provider';
import useOriginNavigate from './useOriginNavigate';
import { mainTextareaId } from '~/common'; import { mainTextareaId } from '~/common';
import store from '~/store'; import store from '~/store';
const useNewConvo = (index = 0) => { const useNewConvo = (index = 0) => {
const navigate = useOriginNavigate(); const navigate = useNavigate();
const { data: startupConfig } = useGetStartupConfig(); const { data: startupConfig } = useGetStartupConfig();
const defaultPreset = useRecoilValue(store.defaultPreset); const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index); const { setConversation } = store.useCreateConversationAtom(index);
@ -147,7 +148,7 @@ const useNewConvo = (index = 0) => {
if (appTitle) { if (appTitle) {
document.title = appTitle; document.title = appTitle;
} }
navigate('new'); navigate('/c/new');
} }
clearTimeout(timeoutIdRef.current); clearTimeout(timeoutIdRef.current);

View file

@ -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;

View file

@ -20,6 +20,9 @@ export default {
com_sidepanel_attach_files: 'Attach Files', com_sidepanel_attach_files: 'Attach Files',
com_sidepanel_manage_files: 'Manage Files', com_sidepanel_manage_files: 'Manage Files',
com_assistants_capabilities: 'Capabilities', 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: 'Knowledge',
com_assistants_knowledge_info: com_assistants_knowledge_info:
'If you upload files under Knowledge, conversations with your Assistant may include file contents.', '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_actions: 'Actions',
com_assistants_add_tools: 'Add Tools', com_assistants_add_tools: 'Add Tools',
com_assistants_add_actions: 'Add Actions', 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_available_actions: 'Available Actions',
com_assistants_running_action: 'Running action', com_assistants_running_action: 'Running action',
com_assistants_completed_action: 'Talked to {0}', com_assistants_completed_action: 'Talked to {0}',
@ -73,6 +78,8 @@ export default {
com_ui_field_required: 'This field is required', com_ui_field_required: 'This field is required',
com_ui_download_error: 'Error downloading file. The file may have been deleted.', 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_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_size: 'File size limit exceeded for endpoint:',
com_ui_attach_error: com_ui_attach_error:
'Cannot attach file. Create or select a conversation, or try refreshing the page.', 'Cannot attach file. Create or select a conversation, or try refreshing the page.',

View file

@ -271,3 +271,7 @@
-webkit-animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1); -webkit-animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1);
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);
}

View file

@ -41,7 +41,7 @@ registration:
endpoints: endpoints:
# assistants: # assistants:
# disableBuilder: false # Disable Assistants Builder Interface by setting to `true` # 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 # timeoutMs: 180000 # Timeout for assistant operations
# # Should only be one or the other, either `supportedIds` or `excludedIds` # # Should only be one or the other, either `supportedIds` or `excludedIds`
# supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"] # supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"]

View file

@ -1,6 +1,6 @@
{ {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.6.4", "version": "0.6.5",
"description": "data services for librechat apps", "description": "data services for librechat apps",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -683,7 +683,7 @@ export enum Constants {
/** Key for the app's version. */ /** Key for the app's version. */
VERSION = 'v0.7.2', VERSION = 'v0.7.2',
/** Key for the Custom Config's version (librechat.yaml). */ /** 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. */ /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
NO_PARENT = '00000000-0000-0000-0000-000000000000', NO_PARENT = '00000000-0000-0000-0000-000000000000',
/** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */ /** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */

View file

@ -334,9 +334,10 @@ export const getFileDownload = async (userId: string, file_id: string): Promise<
export const deleteFiles = async ( export const deleteFiles = async (
files: f.BatchFile[], files: f.BatchFile[],
assistant_id?: string, assistant_id?: string,
tool_resource?: a.EToolResources,
): Promise<f.DeleteFilesResponse> => ): Promise<f.DeleteFilesResponse> =>
request.deleteWithOptions(endpoints.files(), { request.deleteWithOptions(endpoints.files(), {
data: { files, assistant_id }, data: { files, assistant_id, tool_resource },
}); });
/* actions */ /* actions */

View file

@ -16,6 +16,11 @@ export enum Tools {
function = 'function', function = 'function',
} }
export enum EToolResources {
code_interpreter = 'code_interpreter',
file_search = 'file_search',
}
export type Tool = { export type Tool = {
[type: string]: Tools; [type: string]: Tools;
}; };
@ -94,6 +99,7 @@ export type AssistantUpdateParams = {
metadata?: Metadata | null; metadata?: Metadata | null;
name?: string | null; name?: string | null;
tools?: Array<FunctionTool | string>; tools?: Array<FunctionTool | string>;
tool_resources?: ToolResources;
endpoint: AssistantsEndpoint; endpoint: AssistantsEndpoint;
}; };

View file

@ -1,3 +1,5 @@
import { EToolResources } from './assistants';
export enum FileSources { export enum FileSources {
local = 'local', local = 'local',
firebase = 'firebase', firebase = 'firebase',
@ -7,6 +9,9 @@ export enum FileSources {
vectordb = 'vectordb', vectordb = 'vectordb',
} }
export const checkOpenAIStorage = (source: string) =>
source === FileSources.openai || source === FileSources.azure;
export enum FileContext { export enum FileContext {
avatar = 'avatar', avatar = 'avatar',
unknown = 'unknown', unknown = 'unknown',
@ -55,6 +60,7 @@ export type TFile = {
usage: number; usage: number;
context?: FileContext; context?: FileContext;
source?: FileSources; source?: FileSources;
filterSource?: FileSources;
width?: number; width?: number;
height?: number; height?: number;
expiresAt?: string | Date; expiresAt?: string | Date;
@ -98,6 +104,7 @@ export type BatchFile = {
export type DeleteFilesBody = { export type DeleteFilesBody = {
files: BatchFile[]; files: BatchFile[];
assistant_id?: string; assistant_id?: string;
tool_resource?: EToolResources;
}; };
export type DeleteMutationOptions = { export type DeleteMutationOptions = {