mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
* 👁️ feat: Add Azure Mistral OCR strategy and endpoint integration This commit introduces a new OCR strategy named 'azure_mistral_ocr', allowing the use of a Mistral OCR endpoint deployed on Azure. The configuration, schemas, and file upload strategies have been updated to support this integration, enabling seamless OCR processing via Azure-hosted Mistral services. * 🗑️ chore: Clean up .gitignore by removing commented-out uncommon directory name * chore: remove unused vars * refactor: Move createAxiosInstance to packages/api/utils and update imports - Removed the createAxiosInstance function from the config module and relocated it to a new utils module for better organization. - Updated import paths in relevant files to reflect the new location of createAxiosInstance. - Added tests for createAxiosInstance to ensure proper functionality and proxy configuration handling. * chore: move axios helpers to packages/api - Added logAxiosError function to @librechat/api for centralized error logging. - Updated imports across various files to use the new logAxiosError function. - Removed the old axios.js utility file as it is no longer needed. * chore: Update Jest moduleNameMapper for improved path resolution - Added a new mapping for '~/' to resolve module paths in Jest configuration, enhancing import handling for the project. * feat: Implement Mistral OCR API integration in TS * chore: Update MistralOCR tests based on new imports * fix: Enhance MistralOCR configuration handling and tests - Introduced helper functions for resolving configuration values from environment variables or hardcoded settings. - Updated the uploadMistralOCR and uploadAzureMistralOCR functions to utilize the new configuration resolution logic. - Improved test cases to ensure correct behavior when mixing environment variables and hardcoded values. - Mocked file upload and signed URL responses in tests to validate functionality without external dependencies. * feat: Enhance MistralOCR functionality with improved configuration and error handling - Introduced helper functions for loading authentication configuration and resolving values from environment variables. - Updated uploadMistralOCR and uploadAzureMistralOCR functions to utilize the new configuration logic. - Added utility functions for processing OCR results and creating error messages. - Improved document type determination and result aggregation for better OCR processing. * refactor: Reorganize OCR type imports in Mistral CRUD file - Moved OCRResult, OCRResultPage, and OCRImage imports to a more logical grouping for better readability and maintainability. * feat: Add file exports to API and create files index * chore: Update OCR types for enhanced structure and clarity - Redesigned OCRImage interface to include mandatory fields and improved naming conventions. - Added PageDimensions interface for better representation of page metrics. - Updated OCRResultPage to include dimensions and mandatory images array. - Refined OCRResult to include document annotation and usage information. * refactor: use TS counterpart of uploadOCR methods * ci: Update MistralOCR tests to reflect new OCR result structure * chore: Bump version of @librechat/api to 1.2.3 in package.json and package-lock.json * chore: Update CONFIG_VERSION to 1.2.8 * chore: remove unused sendEvent function from config module (now imported from '@librechat/api') * chore: remove MistralOCR service files and tests (now in '@librechat/api') * ci: update logger import in ModelService tests to use @librechat/data-schemas --------- Co-authored-by: arthurolivierfortin <arthurolivier.fortin@gmail.com>
251 lines
7.8 KiB
JavaScript
251 lines
7.8 KiB
JavaScript
const path = require('path');
|
|
const { v4 } = require('uuid');
|
|
const axios = require('axios');
|
|
const { logAxiosError } = require('@librechat/api');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { getCodeBaseURL } = require('@librechat/agents');
|
|
const {
|
|
Tools,
|
|
FileContext,
|
|
FileSources,
|
|
imageExtRegex,
|
|
EToolResources,
|
|
} = require('librechat-data-provider');
|
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
|
const { convertImage } = require('~/server/services/Files/images/convert');
|
|
const { createFile, getFiles, updateFile } = require('~/models/File');
|
|
|
|
/**
|
|
* Process OpenAI image files, convert to target format, save and return file metadata.
|
|
* @param {ServerRequest} params.req - The Express request object.
|
|
* @param {string} params.id - The file ID.
|
|
* @param {string} params.name - The filename.
|
|
* @param {string} params.apiKey - The code execution API key.
|
|
* @param {string} params.toolCallId - The tool call ID that generated the file.
|
|
* @param {string} params.session_id - The code execution session ID.
|
|
* @param {string} params.conversationId - The current conversation ID.
|
|
* @param {string} params.messageId - The current message ID.
|
|
* @returns {Promise<MongoFile & { messageId: string, toolCallId: string } | { filename: string; filepath: string; expiresAt: number; conversationId: string; toolCallId: string; messageId: string } | undefined>} The file metadata or undefined if an error occurs.
|
|
*/
|
|
const processCodeOutput = async ({
|
|
req,
|
|
id,
|
|
name,
|
|
apiKey,
|
|
toolCallId,
|
|
conversationId,
|
|
messageId,
|
|
session_id,
|
|
}) => {
|
|
const currentDate = new Date();
|
|
const baseURL = getCodeBaseURL();
|
|
const fileExt = path.extname(name);
|
|
if (!fileExt || !imageExtRegex.test(name)) {
|
|
return {
|
|
filename: name,
|
|
filepath: `/api/files/code/download/${session_id}/${id}`,
|
|
/** Note: expires 24 hours after creation */
|
|
expiresAt: currentDate.getTime() + 86400000,
|
|
conversationId,
|
|
toolCallId,
|
|
messageId,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const formattedDate = currentDate.toISOString();
|
|
const response = await axios({
|
|
method: 'get',
|
|
url: `${baseURL}/download/${session_id}/${id}`,
|
|
responseType: 'arraybuffer',
|
|
headers: {
|
|
'User-Agent': 'LibreChat/1.0',
|
|
'X-API-Key': apiKey,
|
|
},
|
|
timeout: 15000,
|
|
});
|
|
|
|
const buffer = Buffer.from(response.data, 'binary');
|
|
|
|
const file_id = v4();
|
|
const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`);
|
|
const file = {
|
|
..._file,
|
|
file_id,
|
|
usage: 1,
|
|
filename: name,
|
|
conversationId,
|
|
user: req.user.id,
|
|
type: `image/${req.app.locals.imageOutputType}`,
|
|
createdAt: formattedDate,
|
|
updatedAt: formattedDate,
|
|
source: req.app.locals.fileStrategy,
|
|
context: FileContext.execute_code,
|
|
};
|
|
createFile(file, true);
|
|
/** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */
|
|
return Object.assign(file, { messageId, toolCallId });
|
|
} catch (error) {
|
|
logAxiosError({
|
|
message: 'Error downloading code environment file',
|
|
error,
|
|
});
|
|
}
|
|
};
|
|
|
|
function checkIfActive(dateString) {
|
|
const givenDate = new Date(dateString);
|
|
const currentDate = new Date();
|
|
const timeDifference = currentDate - givenDate;
|
|
const hoursPassed = timeDifference / (1000 * 60 * 60);
|
|
return hoursPassed < 23;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the `lastModified` time string for a specified file from Code Execution Server.
|
|
*
|
|
* @param {Object} params - The parameters object.
|
|
* @param {string} params.fileIdentifier - The identifier for the file (e.g., "session_id/fileId").
|
|
* @param {string} params.apiKey - The API key for authentication.
|
|
*
|
|
* @returns {Promise<string|null>}
|
|
* A promise that resolves to the `lastModified` time string of the file if successful, or null if there is an
|
|
* error in initialization or fetching the info.
|
|
*/
|
|
async function getSessionInfo(fileIdentifier, apiKey) {
|
|
try {
|
|
const baseURL = getCodeBaseURL();
|
|
const [path, queryString] = fileIdentifier.split('?');
|
|
const session_id = path.split('/')[0];
|
|
|
|
let queryParams = {};
|
|
if (queryString) {
|
|
queryParams = Object.fromEntries(new URLSearchParams(queryString).entries());
|
|
}
|
|
|
|
const response = await axios({
|
|
method: 'get',
|
|
url: `${baseURL}/files/${session_id}`,
|
|
params: {
|
|
detail: 'summary',
|
|
...queryParams,
|
|
},
|
|
headers: {
|
|
'User-Agent': 'LibreChat/1.0',
|
|
'X-API-Key': apiKey,
|
|
},
|
|
timeout: 5000,
|
|
});
|
|
|
|
return response.data.find((file) => file.name.startsWith(path))?.lastModified;
|
|
} catch (error) {
|
|
logAxiosError({
|
|
message: `Error fetching session info: ${error.message}`,
|
|
error,
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {ServerRequest} options.req
|
|
* @param {Agent['tool_resources']} options.tool_resources
|
|
* @param {string} apiKey
|
|
* @returns {Promise<{
|
|
* files: Array<{ id: string; session_id: string; name: string }>,
|
|
* toolContext: string,
|
|
* }>}
|
|
*/
|
|
const primeFiles = async (options, apiKey) => {
|
|
const { tool_resources } = options;
|
|
const file_ids = tool_resources?.[EToolResources.execute_code]?.file_ids ?? [];
|
|
const agentResourceIds = new Set(file_ids);
|
|
const resourceFiles = tool_resources?.[EToolResources.execute_code]?.files ?? [];
|
|
const dbFiles = ((await getFiles({ file_id: { $in: file_ids } })) ?? []).concat(resourceFiles);
|
|
|
|
const files = [];
|
|
const sessions = new Map();
|
|
let toolContext = '';
|
|
|
|
for (let i = 0; i < dbFiles.length; i++) {
|
|
const file = dbFiles[i];
|
|
if (!file) {
|
|
continue;
|
|
}
|
|
|
|
if (file.metadata.fileIdentifier) {
|
|
const [path, queryString] = file.metadata.fileIdentifier.split('?');
|
|
const [session_id, id] = path.split('/');
|
|
|
|
const pushFile = () => {
|
|
if (!toolContext) {
|
|
toolContext = `- Note: The following files are available in the "${Tools.execute_code}" tool environment:`;
|
|
}
|
|
toolContext += `\n\t- /mnt/data/${file.filename}${
|
|
agentResourceIds.has(file.file_id) ? '' : ' (just attached by user)'
|
|
}`;
|
|
files.push({
|
|
id,
|
|
session_id,
|
|
name: file.filename,
|
|
});
|
|
};
|
|
|
|
if (sessions.has(session_id)) {
|
|
pushFile();
|
|
continue;
|
|
}
|
|
|
|
let queryParams = {};
|
|
if (queryString) {
|
|
queryParams = Object.fromEntries(new URLSearchParams(queryString).entries());
|
|
}
|
|
|
|
const reuploadFile = async () => {
|
|
try {
|
|
const { getDownloadStream } = getStrategyFunctions(file.source);
|
|
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(
|
|
FileSources.execute_code,
|
|
);
|
|
const stream = await getDownloadStream(options.req, file.filepath);
|
|
const fileIdentifier = await uploadCodeEnvFile({
|
|
req: options.req,
|
|
stream,
|
|
filename: file.filename,
|
|
entity_id: queryParams.entity_id,
|
|
apiKey,
|
|
});
|
|
await updateFile({ file_id: file.file_id, metadata: { fileIdentifier } });
|
|
sessions.set(session_id, true);
|
|
pushFile();
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error re-uploading file ${id} in session ${session_id}: ${error.message}`,
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
const uploadTime = await getSessionInfo(file.metadata.fileIdentifier, apiKey);
|
|
if (!uploadTime) {
|
|
logger.warn(`Failed to get upload time for file ${id} in session ${session_id}`);
|
|
await reuploadFile();
|
|
continue;
|
|
}
|
|
if (!checkIfActive(uploadTime)) {
|
|
await reuploadFile();
|
|
continue;
|
|
}
|
|
sessions.set(session_id, true);
|
|
pushFile();
|
|
}
|
|
}
|
|
|
|
return { files, toolContext };
|
|
};
|
|
|
|
module.exports = {
|
|
primeFiles,
|
|
processCodeOutput,
|
|
};
|