mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-25 03:44:09 +01:00
📄 feat: Local Text Extraction for PDF, DOCX, and XLS/XLSX (#11900)
* feat: Added "document parser" OCR strategy The document parser uses libraries to parse the text out of known document types. This lets LibreChat handle some complex document types without having to use a secondary service (like Mistral or standing up a RAG API server). To enable the document parser, set the ocr strategy to "document_parser" in librechat.yaml. We now support: - PDFs using pdfjs - DOCX using mammoth - XLS/XLSX using SheetJS (The associated packages were also added to the project.) * fix: applied Copilot code review suggestions - Properly calculate length of text based on UTF8. - Avoid issues with loading / blocking PDF parsing. * fix: improved docs on parseDocument() * chore: move to packages/api for TS support * refactor: make document processing the default ocr strategy - Introduced support for additional document types in the OCR strategy, including PDF, DOCX, and XLS/XLSX. - Updated the file upload handling to dynamically select the appropriate parsing strategy based on the file type. - Refactored the document parsing functions to use asynchronous imports for improved performance and maintainability. * test: add unit tests for processAgentFileUpload functionality - Introduced a new test suite for the processAgentFileUpload function in process.spec.js. - Implemented various test cases to validate OCR strategy selection based on file types, including PDF, DOCX, XLSX, and XLS. - Mocked dependencies to ensure isolated testing of file upload handling and strategy selection logic. - Enhanced coverage for scenarios involving OCR capability checks and default strategy fallbacks. * chore: update pdfjs-dist version and enhance document parsing tests - Bumped pdfjs-dist dependency to version 5.4.624 in both api and packages/api. - Refactored document parsing tests to use 'originalname' instead of 'filename' for file objects. - Added a new test case for parsing XLS files to improve coverage of document types supported by the parser. - Introduced a sample XLS file for testing purposes. * feat: enforce text size limit and improve OCR fallback handling in processAgentFileUpload - Added a check to ensure extracted text does not exceed the 15MB storage limit, throwing an error if it does. - Refactored the OCR handling logic to improve fallback behavior when the configured OCR fails, ensuring a more robust document processing flow. - Enhanced unit tests to cover scenarios for oversized text and fallback mechanisms, ensuring proper error handling and functionality. * fix: correct OCR URL construction in performOCR function - Updated the OCR URL construction to ensure it correctly appends '/ocr' to the base URL if not already present, improving the reliability of the OCR request. --------- Co-authored-by: Dan Lew <daniel@mightyacorn.com>
This commit is contained in:
parent
7692fa837e
commit
7ce898d6a0
16 changed files with 1012 additions and 25 deletions
|
|
@ -523,6 +523,12 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
|
|||
* @return {Promise<void>}
|
||||
*/
|
||||
const createTextFile = async ({ text, bytes, filepath, type = 'text/plain' }) => {
|
||||
const textBytes = Buffer.byteLength(text, 'utf8');
|
||||
if (textBytes > 15 * megabyte) {
|
||||
throw new Error(
|
||||
`Extracted text from "${file.originalname}" exceeds the 15MB storage limit (${Math.round(textBytes / megabyte)}MB). Try a shorter document.`,
|
||||
);
|
||||
}
|
||||
const fileInfo = removeNullishValues({
|
||||
text,
|
||||
bytes,
|
||||
|
|
@ -553,29 +559,59 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
|
|||
|
||||
const fileConfig = mergeFileConfig(appConfig.fileConfig);
|
||||
|
||||
const shouldUseOCR =
|
||||
const documentParserMimeTypes = [
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
];
|
||||
|
||||
const shouldUseConfiguredOCR =
|
||||
appConfig?.ocr != null &&
|
||||
fileConfig.checkType(file.mimetype, fileConfig.ocr?.supportedMimeTypes || []);
|
||||
|
||||
if (shouldUseOCR && !(await checkCapability(req, AgentCapabilities.ocr))) {
|
||||
throw new Error('OCR capability is not enabled for Agents');
|
||||
} else if (shouldUseOCR) {
|
||||
const shouldUseDocumentParser =
|
||||
!shouldUseConfiguredOCR && documentParserMimeTypes.includes(file.mimetype);
|
||||
|
||||
const shouldUseOCR = shouldUseConfiguredOCR || shouldUseDocumentParser;
|
||||
|
||||
const resolveDocumentText = async () => {
|
||||
if (shouldUseConfiguredOCR) {
|
||||
try {
|
||||
const ocrStrategy = appConfig?.ocr?.strategy ?? FileSources.document_parser;
|
||||
const { handleFileUpload } = getStrategyFunctions(ocrStrategy);
|
||||
return await handleFileUpload({ req, file, loadAuthValues });
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[processAgentFileUpload] Configured OCR failed for "${file.originalname}", falling back to document_parser:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { handleFileUpload: uploadOCR } = getStrategyFunctions(
|
||||
appConfig?.ocr?.strategy ?? FileSources.mistral_ocr,
|
||||
);
|
||||
const {
|
||||
text,
|
||||
bytes,
|
||||
filepath: ocrFileURL,
|
||||
} = await uploadOCR({ req, file, loadAuthValues });
|
||||
return await createTextFile({ text, bytes, filepath: ocrFileURL });
|
||||
} catch (ocrError) {
|
||||
const { handleFileUpload } = getStrategyFunctions(FileSources.document_parser);
|
||||
return await handleFileUpload({ req, file, loadAuthValues });
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[processAgentFileUpload] OCR processing failed for file "${file.originalname}", falling back to text extraction:`,
|
||||
ocrError,
|
||||
`[processAgentFileUpload] Document parser failed for "${file.originalname}":`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldUseConfiguredOCR && !(await checkCapability(req, AgentCapabilities.ocr))) {
|
||||
throw new Error('OCR capability is not enabled for Agents');
|
||||
}
|
||||
|
||||
if (shouldUseOCR) {
|
||||
const ocrResult = await resolveDocumentText();
|
||||
if (ocrResult) {
|
||||
const { text, bytes, filepath: ocrFileURL } = ocrResult;
|
||||
return await createTextFile({ text, bytes, filepath: ocrFileURL });
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to extract text from "${file.originalname}". The document may be image-based and requires an OCR service to process.`,
|
||||
);
|
||||
}
|
||||
|
||||
const shouldUseSTT = fileConfig.checkType(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue