Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable azure v2 assistant with file search #5031

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions api/server/controllers/assistants/v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,90 @@ const deleteResourceFileId = async ({ req, openai, assistant_id, tool_resource,
});
};

/**
* Modifies an assistant with the resource vector store ID.
* @param {object} params
* @param {Express.Request} params.req
* @param {OpenAIClient} params.openai
* @param {string} params.assistant_id
* @param {string} params.tool_resource
* @param {string} params.vector_store_id
* @returns {Promise<Assistant>} The updated assistant.
*/
const addResourceVectorId = async ({
req,
openai,
assistant_id,
tool_resource,
vector_store_id,
}) => {
const assistant = await openai.beta.assistants.retrieve(assistant_id);
const { tool_resources = {} } = assistant;

if (tool_resources[tool_resource]) {
// Replace the vector_store_id if already exists
tool_resources[tool_resource].vector_store_ids.push(vector_store_id);
} else {
// Initialize with the new vector_store_id
tool_resources[tool_resource] = { vector_store_ids: [vector_store_id] };
}

delete assistant.id;
return await updateAssistant({
req,
openai,
assistant_id,
updateData: { tools: assistant.tools, tool_resources },
});
};

/**
* Deletes a vector store ID from an assistant's resource.
* @param {object} params
* @param {Express.Request} params.req
* @param {OpenAIClient} params.openai
* @param {string} params.assistant_id
* @param {string} [params.tool_resource]
* @param {string} params.vector_store_id
* @param {AssistantUpdateParams} params.updateData
* @returns {Promise<Assistant>} The updated assistant.
*/
const deleteResourceVectorId = async ({
req,
openai,
assistant_id,
tool_resource,
vector_store_id,
}) => {
const assistant = await openai.beta.assistants.retrieve(assistant_id);
const { tool_resources = {} } = assistant;

if (tool_resource && tool_resources[tool_resource]) {
const resource = tool_resources[tool_resource];
const index = resource.vector_store_ids?.indexOf(vector_store_id);
if (index !== -1) {
resource.vector_store_ids.splice(index, 1);
}
} else {
for (const resourceKey in tool_resources) {
const resource = tool_resources[resourceKey];
const index = resource.vector_store_ids?.indexOf(vector_store_id);
if (index !== -1) {
resource.vector_store_ids.splice(index, 1);
break;
}
}
}

delete assistant.id;
return await updateAssistant({
req,
openai,
assistant_id,
updateData: { tools: assistant.tools, tool_resources },
});
};

/**
* Modifies an assistant.
* @route PATCH /assistants/:id
Expand Down Expand Up @@ -260,4 +344,6 @@ module.exports = {
updateAssistant,
addResourceFileId,
deleteResourceFileId,
addResourceVectorId,
deleteResourceVectorId,
};
89 changes: 89 additions & 0 deletions api/server/services/Files/VectorStore/crud.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const fs = require('fs');
const { FilePurpose } = require('librechat-data-provider');
const axios = require('axios');
const { getOpenAIClient } = require('../../../controllers/assistants/helpers');
const { logger } = require('~/config');

/**
*
* @param {OpenAIClient} openai - The initialized OpenAI client.
* @returns
*/
async function createVectorStore(openai) {
try {
const response = await openai.beta.vectorStores.create({
name: 'Financial Statements',
});
return response.id;
} catch (error) {
logger.error('[createVectorStore] Error creating vector store:', error.message);
throw error;
}
}

/**
* Uploads a file to Azure OpenAI Vector Store for file search.
*
* @param {Object} params - The parameters for the upload.
* @param {Express.Multer.File} params.file - The file uploaded to the server via multer.
* @param {OpenAIClient} params.openai - The initialized OpenAI client.
* @param {string} [params.vectorStoreId] - The ID of the vector store.
* @returns {Promise<Object>} The response from Azure containing the file details.
*/
async function uploadToVectorStore({ openai, file, vectorStoreId }) {
try {
const filePath = file.path;
const fileStreams = [fs.createReadStream(filePath)];
const response = await openai.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
files: fileStreams,
});
logger.debug(
`[uploadToVectorStore] Successfully uploaded file to Azure Vector Store: ${response.id}`,
);
return {
id: response.vector_store_id,
};
} catch (error) {
logger.error('[uploadToVectorStore] Error uploading file:', error.message);
throw new Error(`Failed to upload file to Vector Store: ${error.message}`);
}
}

/**
* Deletes a file from Azure OpenAI Vector Store.
*
* @param {string} file_id - The ID of the file to delete.
* @param {string} vectorStoreId - The ID of the vector store.
* @returns {Promise<void>}
*/
async function deleteFromVectorStore(file_id, vectorStoreId) {
try {
// Get OpenAI client directly
const { openai } = await getOpenAIClient();
const azureOpenAIEndpoint = openai.baseURL;
const azureOpenAIKey = openai.apiKey;

const response = await axios.delete(
`${azureOpenAIEndpoint}/vector_stores/${vectorStoreId}/files/${file_id}?api-version=2024-10-01-preview`,
{
headers: {
'api-key': azureOpenAIKey,
'Content-Type': 'application/json',
},
},
);

if (!response.data.deleted) {
throw new Error(`Failed to delete file ${file_id} from Azure Vector Store`);
}

logger.debug(
`[deleteFromVectorStore] Successfully deleted file ${file_id} from Azure Vector Store`,
);
} catch (error) {
logger.error('[deleteFromVectorStore] Error deleting file:', error.message);
throw error;
}
}

module.exports = { uploadToVectorStore, deleteFromVectorStore, createVectorStore };
5 changes: 5 additions & 0 deletions api/server/services/Files/VectorStore/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const crud = require('./crud');

module.exports = {
...crud,
};
49 changes: 33 additions & 16 deletions api/server/services/Files/process.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ const {
isAssistantsEndpoint,
} = require('librechat-data-provider');
const { EnvVar } = require('@librechat/agents');
const {
addResourceFileId,
deleteResourceFileId,
addResourceVectorId,
} = require('~/server/controllers/assistants/v2');
const {
convertImage,
resizeAndConvert,
resizeImageBuffer,
resizeImageBuffer
} = require('~/server/services/Files/images');
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
Expand Down Expand Up @@ -341,9 +345,8 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true })
inputBuffer: buffer,
desiredFormat: req.app.locals.imageOutputType,
}));
filename = `${path.basename(req.file.originalname, path.extname(req.file.originalname))}.${
req.app.locals.imageOutputType
}`;
filename = `${path.basename(req.file.originalname, path.extname(req.file.originalname))}.${req.app.locals.imageOutputType
}`;
}

const filepath = await saveBuffer({ userId: req.user.id, fileName: filename, buffer });
Expand Down Expand Up @@ -379,14 +382,21 @@ const processFileUpload = async ({ req, res, metadata }) => {
const isAssistantUpload = isAssistantsEndpoint(metadata.endpoint);
const assistantSource =
metadata.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai;
const source = isAssistantUpload ? assistantSource : FileSources.vectordb;
const { handleFileUpload } = getStrategyFunctions(source);
const fileSource =
isAssistantUpload && metadata.tool_resource === EToolResources.file_search
? FileSources.vector_store
: assistantSource;
const source = isAssistantUpload ? fileSource : FileSources.vectordb;
const { handleFileUpload, createStore } = getStrategyFunctions(source);
const { file_id, temp_file_id } = metadata;

/** @type {OpenAI | undefined} */
let openai;
if (checkOpenAIStorage(source)) {
({ openai } = await getOpenAIClient({ req }));
/** @type {{ openai: OpenAIClient }} */
let { openai } = await getOpenAIClient({ req, res });

let vector_id;

if (source === FileSources.vector_store && typeof createStore === 'function') {
vector_id = await createStore(openai);
}

const { file } = req;
Expand All @@ -403,12 +413,21 @@ const processFileUpload = async ({ req, res, metadata }) => {
file,
file_id,
openai,
...(source === FileSources.vector_store && { vectorStoreId: vector_id }),
});

if (isAssistantUpload && !metadata.message_file && !metadata.tool_resource) {
await openai.beta.assistants.files.create(metadata.assistant_id, {
file_id: id,
});
} else if (isAssistantUpload && metadata.tool_resource === EToolResources.file_search) {
await addResourceVectorId({
req,
openai,
vector_store_id: id,
assistant_id: metadata.assistant_id,
tool_resource: metadata.tool_resource,
});
} else if (isAssistantUpload && !metadata.message_file) {
await addResourceFileId({
req,
Expand Down Expand Up @@ -577,9 +596,8 @@ const processOpenAIFile = async ({
}) => {
const _file = await openai.files.retrieve(file_id);
const originalName = filename ?? (_file.filename ? path.basename(_file.filename) : undefined);
const filepath = `${openai.baseURL}/files/${userId}/${file_id}${
originalName ? `/${originalName}` : ''
}`;
const filepath = `${openai.baseURL}/files/${userId}/${file_id}${originalName ? `/${originalName}` : ''
}`;
const type = mime.getType(originalName ?? file_id);
const source =
openai.req.body.endpoint === EModelEndpoint.azureAssistants
Expand Down Expand Up @@ -854,8 +872,7 @@ function filterFile({ req, image, isAvatar }) {

if (file.size > fileSizeLimit) {
throw new Error(
`File size limit of ${fileSizeLimit / megabyte} MB exceeded for ${
isAvatar ? 'avatar upload' : `${endpoint} endpoint`
`File size limit of ${fileSizeLimit / megabyte} MB exceeded for ${isAvatar ? 'avatar upload' : `${endpoint} endpoint`
}`,
);
}
Expand Down
9 changes: 9 additions & 0 deletions api/server/services/Files/strategies.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./Code');
const { uploadVectors, deleteVectors } = require('./VectorDB');
const { uploadToVectorStore, deleteFromVectorStore, createVectorStore } = require('./VectorStore');

/**
* Firebase Storage Strategy Functions
Expand Down Expand Up @@ -103,6 +104,12 @@ const openAIStrategy = () => ({
getDownloadStream: getOpenAIFileStream,
});

const vectorStoreStrategy = () => ({
createStore: createVectorStore,
handleFileUpload: uploadToVectorStore,
deleteFile: deleteFromVectorStore,
});

/**
* Code Output Strategy Functions
*
Expand Down Expand Up @@ -139,6 +146,8 @@ const getStrategyFunctions = (fileSource) => {
return openAIStrategy();
} else if (fileSource === FileSources.vectordb) {
return vectorStrategy();
} else if (fileSource === FileSources.vector_store) {
return vectorStoreStrategy();
} else if (fileSource === FileSources.execute_code) {
return codeOutputStrategy();
} else {
Expand Down
2 changes: 2 additions & 0 deletions client/src/common/assistants-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ export type TAssistantOption =
Assistant & {
files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
search_files?: Array<[string, ExtendedFile]>;
});

export type Actions = {
[Capabilities.code_interpreter]: boolean;
[Capabilities.image_vision]: boolean;
[Capabilities.retrieval]: boolean;
[Capabilities.file_search]: boolean;
};

export type AssistantForm = {
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/SidePanel/Builder/AssistantPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function AssistantPanel({
[assistantsConfig],
);
const retrievalEnabled = useMemo(
() => assistantsConfig?.capabilities?.includes(Capabilities.retrieval),
() => assistantsConfig?.capabilities?.includes(Capabilities.file_search),
[assistantsConfig],
);
const codeEnabled = useMemo(
Expand Down Expand Up @@ -155,6 +155,9 @@ export default function AssistantPanel({
if (data.code_interpreter) {
tools.push({ type: Tools.code_interpreter });
}
if (data.file_search) {
tools.push({ type: Tools.file_search });
}
if (data.retrieval) {
tools.push({ type: version == 2 ? Tools.file_search : Tools.retrieval });
}
Expand Down
Loading