mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* 📎 feat: Direct Provider Attachment Support for Multimodal Content * 📑 feat: Anthropic Direct Provider Upload (#9072) * feat: implement Anthropic native PDF support with document preservation - Add comprehensive debug logging throughout PDF processing pipeline - Refactor attachment processing to separate image and document handling - Create distinct addImageURLs(), addDocuments(), and processAttachments() methods - Fix critical bugs in stream handling and parameter passing - Add streamToBuffer utility for proper stream-to-buffer conversion - Remove api/agents submodule from repository 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: remove out of scope formatting changes * fix: stop duplication of file in chat on end of response stream * chore: bring back file search and ocr options * chore: localize upload to provider string in file menu * refactor: change createMenuItems args to fit new pattern introduced by anthropic-native-pdf-support * feat: add cache point for pdfs processed by anthropic endpoint since they are unlikely to change and should benefit from caching * feat: combine Upload Image into Upload to Provider since they both perform direct upload and change provider upload icon to reflect multimodal upload * feat: add citations support according to docs * refactor: remove redundant 'document' check since documents are handled properly by formatMessage in the agents repo now * refactor: change upload logic so anthropic endpoint isn't exempted from normal upload path using Agents for consistency with the rest of the upload logic * fix: include width and height in return from uploadLocalFile so images are correctly identified when going through an AgentUpload in addImageURLs * chore: remove client specific handling since the direct provider stuff is handled by the agent client * feat: handle documents in AgentClient so no need for change to agents repo * chore: removed unused changes * chore: remove auto generated comments from OG commit * feat: add logic for agents to use direct to provider uploads if supported (currently just anthropic) * fix: reintroduce role check to fix render error because of undefined value for Content Part * fix: actually fix render bug by using proper isCreatedByUser check and making sure our mutation of formattedMessage.content is consistent --------- Co-authored-by: Andres Restrepo <andres@thelinuxkid.com> Co-authored-by: Claude <noreply@anthropic.com> 📁 feat: Send Attachments Directly to Provider (OpenAI) (#9098) * refactor: change references from direct upload to direct attach to better reflect functionality since we are just using base64 encoding strategy now rather than Files/File API for sending our attachments directly to the provider, the upload nomenclature no longer makes sense. direct_attach better describes the different methods of sending attachments to providers anyways even if we later introduce direct upload support * feat: add upload to provider option for openai (and agent) ui * chore: move anthropic pdf validator over to packages/api * feat: simple pdf validation according to openai docs * feat: add provider agnostic validatePdf logic to start handling multiple endpoints * feat: add handling for openai specific documentPart formatting * refactor: move require statement to proper place at top of file * chore: add in openAI endpoint for the rest of the document handling logic * feat: add direct attach support for azureOpenAI endpoint and agents * feat: add pdf validation for azureOpenAI endpoint * refactor: unify all the endpoint checks with isDocumentSupportedEndpoint * refactor: consolidate Upload to Provider vs Upload image logic for clarity * refactor: remove anthropic from anthropic_multimodal fileType since we support multiple providers now 🗂️ feat: Send Attachments Directly to Provider (Google) (#9100) * feat: add validation for google PDFs and add google endpoint as a document supporting endpoint * feat: add proper pdf formatting for google endpoints (requires PR #14 in agents) * feat: add multimodal support for google endpoint attachments * feat: add audio file svg * fix: refactor attachments logic so multi-attachment messages work properly * feat: add video file svg * fix: allows for followup questions of uploaded multimodal attachments * fix: remove incorrect final message filtering that was breaking Attachment component rendering fix: manualy rename 'documents' to 'Documents' in git since it wasn't picked up due to case insensitivity in dir name fix: add logic so filepicker for a google agent has proper filetype filtering 🛫 refactor: Move Encoding Logic to packages/api (#9182) * refactor: move audio encode over to TS * refactor: audio encoding now functional in LC again * refactor: move video encode over to TS * refactor: move document encode over to TS * refactor: video encoding now functional in LC again * refactor: document encoding now functional in LC again * fix: extend file type options in AttachFileMenu to include 'google_multimodal' and update dependency array to include agent?.provider * feat: only accept pdfs if responses api is enabled for openai convos chore: address ESLint comments chore: add missing audio mimetype * fix: type safety for message content parts and improve null handling * chore: reorder AttachFileMenuProps for consistency and clarity * chore: import order in AttachFileMenu * fix: improve null handling for text parts in parseTextParts function * fix: remove no longer used unsupported capability error message for file uploads * fix: OpenAI Direct File Attachment Format * fix: update encodeAndFormatDocuments to support OpenAI responses API and enhance document result types * refactor: broaden providers supported for documents * feat: enhance DragDrop context and modal to support document uploads based on provider capabilities * fix: reorder import statements for consistency in video encoding module --------- Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
321 lines
7.5 KiB
TypeScript
321 lines
7.5 KiB
TypeScript
import {
|
|
TextPaths,
|
|
FilePaths,
|
|
CodePaths,
|
|
AudioPaths,
|
|
VideoPaths,
|
|
SheetPaths,
|
|
} from '@librechat/client';
|
|
import {
|
|
megabyte,
|
|
QueryKeys,
|
|
excelMimeTypes,
|
|
EToolResources,
|
|
codeTypeMapping,
|
|
fileConfig as defaultFileConfig,
|
|
} from 'librechat-data-provider';
|
|
import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider';
|
|
import type { QueryClient } from '@tanstack/react-query';
|
|
import type { ExtendedFile } from '~/common';
|
|
|
|
export const partialTypes = ['text/x-'];
|
|
|
|
const textDocument = {
|
|
paths: TextPaths,
|
|
fill: '#FF5588',
|
|
title: 'Document',
|
|
};
|
|
|
|
const spreadsheet = {
|
|
paths: SheetPaths,
|
|
fill: '#10A37F',
|
|
title: 'Spreadsheet',
|
|
};
|
|
|
|
const codeFile = {
|
|
paths: CodePaths,
|
|
fill: '#FF6E3C',
|
|
// TODO: make this dynamic to the language
|
|
title: 'Code',
|
|
};
|
|
|
|
const artifact = {
|
|
paths: CodePaths,
|
|
fill: '#2D305C',
|
|
title: 'Code',
|
|
};
|
|
|
|
const audioFile = {
|
|
paths: AudioPaths,
|
|
fill: '#FF6B35',
|
|
title: 'Audio',
|
|
};
|
|
|
|
const videoFile = {
|
|
paths: VideoPaths,
|
|
fill: '#8B5CF6',
|
|
title: 'Video',
|
|
};
|
|
|
|
export const fileTypes = {
|
|
/* Category matches */
|
|
file: {
|
|
paths: FilePaths,
|
|
fill: '#0000FF',
|
|
title: 'File',
|
|
},
|
|
text: textDocument,
|
|
txt: textDocument,
|
|
audio: audioFile,
|
|
video: videoFile,
|
|
// application:,
|
|
|
|
/* Partial matches */
|
|
csv: spreadsheet,
|
|
'application/pdf': textDocument,
|
|
pdf: textDocument,
|
|
'text/x-': codeFile,
|
|
artifact: artifact,
|
|
|
|
/* Exact matches */
|
|
// 'application/json':,
|
|
// 'text/html':,
|
|
// 'text/css':,
|
|
// image,
|
|
};
|
|
|
|
// export const getFileType = (type = '') => {
|
|
// let fileType = fileTypes.file;
|
|
// const exactMatch = fileTypes[type];
|
|
// const partialMatch = !exactMatch && partialTypes.find((type) => type.includes(type));
|
|
// const category = (!partialMatch && (type.split('/')[0] ?? 'text') || 'text');
|
|
|
|
// if (exactMatch) {
|
|
// fileType = exactMatch;
|
|
// } else if (partialMatch) {
|
|
// fileType = fileTypes[partialMatch];
|
|
// } else if (fileTypes[category]) {
|
|
// fileType = fileTypes[category];
|
|
// }
|
|
|
|
// if (!fileType) {
|
|
// fileType = fileTypes.file;
|
|
// }
|
|
|
|
// return fileType;
|
|
// };
|
|
|
|
export const getFileType = (
|
|
type = '',
|
|
): {
|
|
paths: React.FC;
|
|
fill: string;
|
|
title: string;
|
|
} => {
|
|
// Direct match check
|
|
if (fileTypes[type]) {
|
|
return fileTypes[type];
|
|
}
|
|
|
|
if (excelMimeTypes.test(type)) {
|
|
return spreadsheet;
|
|
}
|
|
|
|
// Partial match check
|
|
const partialMatch = partialTypes.find((partial) => type.includes(partial));
|
|
if (partialMatch && fileTypes[partialMatch]) {
|
|
return fileTypes[partialMatch];
|
|
}
|
|
|
|
// Category check
|
|
const category = type.split('/')[0] || 'text';
|
|
if (fileTypes[category]) {
|
|
return fileTypes[category];
|
|
}
|
|
|
|
// Default file type
|
|
return fileTypes.file;
|
|
};
|
|
|
|
/**
|
|
* Format a date string to a human readable format
|
|
* @example
|
|
* formatDate('2020-01-01T00:00:00.000Z') // '1 Jan 2020'
|
|
*/
|
|
export function formatDate(dateString: string, isSmallScreen = false) {
|
|
if (!dateString) {
|
|
return '';
|
|
}
|
|
|
|
const date = new Date(dateString);
|
|
|
|
if (isSmallScreen) {
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
year: '2-digit',
|
|
});
|
|
}
|
|
|
|
const months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
];
|
|
|
|
const day = date.getDate();
|
|
const month = months[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
|
|
return `${day} ${month} ${year}`;
|
|
}
|
|
|
|
/**
|
|
* Adds a file to the query cache
|
|
*/
|
|
export function addFileToCache(queryClient: QueryClient, newfile: TFile) {
|
|
const currentFiles = queryClient.getQueryData<TFile[]>([QueryKeys.files]);
|
|
|
|
if (!currentFiles) {
|
|
console.warn('No current files found in cache, skipped updating file query cache');
|
|
return;
|
|
}
|
|
|
|
const fileIndex = currentFiles.findIndex((file) => file.file_id === newfile.file_id);
|
|
|
|
if (fileIndex > -1) {
|
|
console.warn('File already exists in cache, skipped updating file query cache');
|
|
return;
|
|
}
|
|
|
|
queryClient.setQueryData<TFile[]>(
|
|
[QueryKeys.files],
|
|
[
|
|
{
|
|
...newfile,
|
|
},
|
|
...currentFiles,
|
|
],
|
|
);
|
|
}
|
|
|
|
export function formatBytes(bytes: number, decimals = 2) {
|
|
if (bytes === 0) {
|
|
return 0;
|
|
}
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
|
}
|
|
|
|
const { checkType } = defaultFileConfig;
|
|
|
|
export const validateFiles = ({
|
|
files,
|
|
fileList,
|
|
setError,
|
|
endpointFileConfig,
|
|
toolResource,
|
|
fileConfig,
|
|
}: {
|
|
fileList: File[];
|
|
files: Map<string, ExtendedFile>;
|
|
setError: (error: string) => void;
|
|
endpointFileConfig: EndpointFileConfig;
|
|
toolResource?: string;
|
|
fileConfig: FileConfig | null;
|
|
}) => {
|
|
const { fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes } = endpointFileConfig;
|
|
const existingFiles = Array.from(files.values());
|
|
const incomingTotalSize = fileList.reduce((total, file) => total + file.size, 0);
|
|
if (incomingTotalSize === 0) {
|
|
setError('com_error_files_empty');
|
|
return false;
|
|
}
|
|
const currentTotalSize = existingFiles.reduce((total, file) => total + file.size, 0);
|
|
|
|
if (fileLimit && fileList.length + files.size > fileLimit) {
|
|
setError(`You can only upload up to ${fileLimit} files at a time.`);
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < fileList.length; i++) {
|
|
let originalFile = fileList[i];
|
|
let fileType = originalFile.type;
|
|
const extension = originalFile.name.split('.').pop() ?? '';
|
|
const knownCodeType = codeTypeMapping[extension];
|
|
|
|
// Infer MIME type for Known Code files when the type is empty or a mismatch
|
|
if (knownCodeType && (!fileType || fileType !== knownCodeType)) {
|
|
fileType = knownCodeType;
|
|
}
|
|
|
|
// Check if the file type is still empty after the extension check
|
|
if (!fileType) {
|
|
setError('Unable to determine file type for: ' + originalFile.name);
|
|
return false;
|
|
}
|
|
|
|
// Replace empty type with inferred type
|
|
if (originalFile.type !== fileType) {
|
|
const newFile = new File([originalFile], originalFile.name, { type: fileType });
|
|
originalFile = newFile;
|
|
fileList[i] = newFile;
|
|
}
|
|
|
|
let mimeTypesToCheck = supportedMimeTypes;
|
|
if (toolResource === EToolResources.context) {
|
|
mimeTypesToCheck = [
|
|
...(fileConfig?.text?.supportedMimeTypes || []),
|
|
...(fileConfig?.ocr?.supportedMimeTypes || []),
|
|
...(fileConfig?.stt?.supportedMimeTypes || []),
|
|
];
|
|
}
|
|
|
|
if (!checkType(originalFile.type, mimeTypesToCheck)) {
|
|
console.log(originalFile);
|
|
setError('Currently, unsupported file type: ' + originalFile.type);
|
|
return false;
|
|
}
|
|
|
|
if (fileSizeLimit && originalFile.size >= fileSizeLimit) {
|
|
setError(`File size exceeds ${fileSizeLimit / megabyte} MB.`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (totalSizeLimit && currentTotalSize + incomingTotalSize > totalSizeLimit) {
|
|
setError(`The total size of the files cannot exceed ${totalSizeLimit / megabyte} MB.`);
|
|
return false;
|
|
}
|
|
|
|
const combinedFilesInfo = [
|
|
...existingFiles.map(
|
|
(file) =>
|
|
`${file.file?.name ?? file.filename}-${file.size}-${file.type?.split('/')[0] ?? 'file'}`,
|
|
),
|
|
...fileList.map(
|
|
(file: File | undefined) =>
|
|
`${file?.name}-${file?.size}-${file?.type.split('/')[0] ?? 'file'}`,
|
|
),
|
|
];
|
|
|
|
const uniqueFilesSet = new Set(combinedFilesInfo);
|
|
|
|
if (uniqueFilesSet.size !== combinedFilesInfo.length) {
|
|
setError('com_error_files_dupe');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|