mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
* feat: Code Interpreter API & File Search Agent Uploads chore: add back code files wip: first pass, abstract key dialog refactor: influence checkbox on key changes refactor: update localization keys for 'execute code' to 'run code' wip: run code button refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions feat: first pass, API tool calling fix: handle missing toolId in callTool function and return 404 for non-existent tools feat: show code outputs fix: improve error handling in callTool function and log errors fix: handle potential null value for filepath in attachment destructuring fix: normalize language before rendering and prevent null return fix: add loading indicator in RunCode component while executing code feat: add support for conditional code execution in Markdown components feat: attachments refactor: remove bash fix: pass abort signal to graph/run refactor: debounce and rate limit tool call refactor: increase debounce delay for execute function feat: set code output attachments feat: image attachments refactor: apply message context refactor: pass `partIndex` feat: toolCall schema/model/methods feat: block indexing feat: get tool calls chore: imports chore: typing chore: condense type imports feat: get tool calls fix: block indexing chore: typing refactor: update tool calls mapping to support multiple results fix: add unique key to nav link for rendering wip: first pass, tool call results refactor: update query cache from successful tool call mutation style: improve result switcher styling chore: note on using \`.toObject()\` feat: add agent_id field to conversation schema chore: typing refactor: rename agentMap to agentsMap for consistency feat: Agent Name as chat input placeholder chore: bump agents 📦 chore: update @langchain dependencies to latest versions to match agents package 📦 chore: update @librechat/agents dependency to version 1.8.0 fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads feat: upload menu feat: prime message_file resources feat: implement conversation access validation in chat route refactor: remove file parameter from processFileUpload and use req.file instead feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db feat: prevent duplicate message saves by checking savedMessageIds in AgentController refactor: skip legacy RAG API handling for agents feat: add files field to convoSchema refactor: update request type annotations from Express.Request to ServerRequest in file processing functions feat: track conversation files fix: resendFiles, addPreviousAttachments handling feat: add ID validation for session_id and file_id in download route feat: entity_id for code file uploads/downloads fix: code file edge cases feat: delete related tool calls feat: add stream rate handling for LLM configuration feat: enhance system content with attached file information fix: improve error logging in resource priming function * WIP: PoC, sequential agents WIP: PoC Sequential Agents, first pass content data + bump agents package fix: package-lock WIP: PoC, o1 support, refactor bufferString feat: convertJsonSchemaToZod fix: form issues and schema defining erroneous model fix: max length issue on agent form instructions, limit conversation messages to sequential agents feat: add abort signal support to createRun function and AgentClient feat: PoC, hide prior sequential agent steps fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data refactor: use only last contentData, track model for usage data chore: bump agents package fix: content parts issue refactor: filter contentParts to include tool calls and relevant indices feat: show function calls refactor: filter context messages to exclude tool calls when no tools are available to the agent fix: ensure tool call content is not undefined in formatMessages feat: add agent_id field to conversationPreset schema feat: hide sequential agents feat: increase upload toast duration to 10 seconds * refactor: tool context handling & update Code API Key Dialog feat: toolContextMap chore: skipSpecs -> useSpecs ci: fix handleTools tests feat: API Key Dialog * feat: Agent Permissions Admin Controls feat: replace label with button for prompt permission toggle feat: update agent permissions feat: enable experimental agents and streamline capability configuration feat: implement access control for agents and enhance endpoint menu items feat: add welcome message for agent selection in localization feat: add agents permission to access control and update version to 0.7.57 * fix: update types in useAssistantListMap and useMentions hooks for better null handling * feat: mention agents * fix: agent tool resource race conditions when deleting agent tool resource files * feat: add error handling for code execution with user feedback * refactor: rename AdminControls to AdminSettings for clarity * style: add gap to button in AdminSettings for improved layout * refactor: separate agent query hooks and check access to enable fetching * fix: remove unused provider from agent initialization options, creates issue with custom endpoints * refactor: remove redundant/deprecated modelOptions from AgentClient processes * chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json * fix: minor styling issues + agent panel uniformity * fix: agent edge cases when set endpoint is no longer defined * refactor: remove unused cleanup function call from AppService * fix: update link in ApiKeyDialog to point to pricing page * fix: improve type handling and layout calculations in SidePanel component * fix: add missing localization string for agent selection in SidePanel * chore: form styling and localizations for upload filesearch/code interpreter * fix: model selection placeholder logic in AgentConfig component * style: agent capabilities * fix: add localization for provider selection and improve dropdown styling in ModelPanel * refactor: use gpt-4o-mini > gpt-3.5-turbo * fix: agents configuration for loadDefaultInterface and update related tests * feat: DALLE Agents support
405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import debounce from 'lodash/debounce';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
import {
|
|
megabyte,
|
|
QueryKeys,
|
|
EModelEndpoint,
|
|
codeTypeMapping,
|
|
mergeFileConfig,
|
|
isAgentsEndpoint,
|
|
isAssistantsEndpoint,
|
|
defaultAssistantsVersion,
|
|
fileConfig as defaultFileConfig,
|
|
} from 'librechat-data-provider';
|
|
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
|
import type { ExtendedFile, FileSetter } from '~/common';
|
|
import { useUploadFileMutation, useGetFileConfig } from '~/data-provider';
|
|
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
|
import { useToastContext } from '~/Providers/ToastContext';
|
|
import { useChatContext } from '~/Providers/ChatContext';
|
|
import useLocalize from '~/hooks/useLocalize';
|
|
import useUpdateFiles from './useUpdateFiles';
|
|
import { logger } from '~/utils';
|
|
|
|
const { checkType } = defaultFileConfig;
|
|
|
|
type UseFileHandling = {
|
|
overrideEndpoint?: EModelEndpoint;
|
|
fileSetter?: FileSetter;
|
|
fileFilter?: (file: File) => boolean;
|
|
additionalMetadata?: Record<string, string | undefined>;
|
|
};
|
|
|
|
const useFileHandling = (params?: UseFileHandling) => {
|
|
const localize = useLocalize();
|
|
const queryClient = useQueryClient();
|
|
const { showToast } = useToastContext();
|
|
const [errors, setErrors] = useState<string[]>([]);
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
|
|
const [toolResource, setToolResource] = useState<string | undefined>();
|
|
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
|
|
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
|
|
const { addFile, replaceFile, updateFileById, deleteFileById } = useUpdateFiles(
|
|
params?.fileSetter ?? setFiles,
|
|
);
|
|
|
|
const agent_id = params?.additionalMetadata?.agent_id ?? '';
|
|
const assistant_id = params?.additionalMetadata?.assistant_id ?? '';
|
|
|
|
const { data: fileConfig = null } = useGetFileConfig({
|
|
select: (data) => mergeFileConfig(data),
|
|
});
|
|
|
|
const endpoint = useMemo(
|
|
() =>
|
|
params?.overrideEndpoint ?? conversation?.endpointType ?? conversation?.endpoint ?? 'default',
|
|
[params?.overrideEndpoint, conversation?.endpointType, conversation?.endpoint],
|
|
);
|
|
|
|
const { fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes } = useMemo(
|
|
() =>
|
|
fileConfig?.endpoints[endpoint] ??
|
|
fileConfig?.endpoints.default ??
|
|
defaultFileConfig.endpoints[endpoint] ??
|
|
defaultFileConfig.endpoints.default,
|
|
[fileConfig, endpoint],
|
|
);
|
|
|
|
const displayToast = useCallback(() => {
|
|
if (errors.length > 1) {
|
|
const errorList = Array.from(new Set(errors))
|
|
.map((e, i) => `${i > 0 ? '• ' : ''}${localize(e) || e}\n`)
|
|
.join('');
|
|
showToast({
|
|
message: errorList,
|
|
status: 'error',
|
|
duration: 5000,
|
|
});
|
|
} else if (errors.length === 1) {
|
|
const message = localize(errors[0]) || errors[0];
|
|
showToast({
|
|
message,
|
|
status: 'error',
|
|
duration: 5000,
|
|
});
|
|
}
|
|
|
|
setErrors([]);
|
|
}, [errors, showToast, localize]);
|
|
|
|
const debouncedDisplayToast = debounce(displayToast, 250);
|
|
|
|
useEffect(() => {
|
|
if (errors.length > 0) {
|
|
debouncedDisplayToast();
|
|
}
|
|
|
|
return () => debouncedDisplayToast.cancel();
|
|
}, [errors, debouncedDisplayToast]);
|
|
|
|
const uploadFile = useUploadFileMutation(
|
|
{
|
|
onSuccess: (data) => {
|
|
clearUploadTimer(data.temp_file_id);
|
|
console.log('upload success', data);
|
|
if (agent_id) {
|
|
queryClient.refetchQueries([QueryKeys.agent, agent_id]);
|
|
return;
|
|
}
|
|
updateFileById(
|
|
data.temp_file_id,
|
|
{
|
|
progress: 0.9,
|
|
filepath: data.filepath,
|
|
},
|
|
assistant_id ? true : false,
|
|
);
|
|
|
|
setTimeout(() => {
|
|
updateFileById(
|
|
data.temp_file_id,
|
|
{
|
|
progress: 1,
|
|
file_id: data.file_id,
|
|
temp_file_id: data.temp_file_id,
|
|
filepath: data.filepath,
|
|
type: data.type,
|
|
height: data.height,
|
|
width: data.width,
|
|
filename: data.filename,
|
|
source: data.source,
|
|
embedded: data.embedded,
|
|
},
|
|
assistant_id ? true : false,
|
|
);
|
|
}, 300);
|
|
},
|
|
onError: (_error, body) => {
|
|
const error = _error as TError | undefined;
|
|
console.log('upload error', error);
|
|
const file_id = body.get('file_id');
|
|
clearUploadTimer(file_id as string);
|
|
deleteFileById(file_id as string);
|
|
const errorMessage =
|
|
error?.code === 'ERR_CANCELED'
|
|
? 'com_error_files_upload_canceled'
|
|
: error?.response?.data?.message ?? 'com_error_files_upload';
|
|
setError(errorMessage);
|
|
},
|
|
onMutate: () => {
|
|
setToolResource(undefined);
|
|
},
|
|
},
|
|
abortControllerRef.current?.signal,
|
|
);
|
|
|
|
const startUpload = async (extendedFile: ExtendedFile) => {
|
|
const filename = extendedFile.file?.name ?? 'File';
|
|
startUploadTimer(extendedFile.file_id, filename, extendedFile.size);
|
|
|
|
const formData = new FormData();
|
|
formData.append('endpoint', endpoint);
|
|
formData.append('file', extendedFile.file as File, encodeURIComponent(filename));
|
|
formData.append('file_id', extendedFile.file_id);
|
|
|
|
const width = extendedFile.width ?? 0;
|
|
const height = extendedFile.height ?? 0;
|
|
if (width) {
|
|
formData.append('width', width.toString());
|
|
}
|
|
if (height) {
|
|
formData.append('height', height.toString());
|
|
}
|
|
|
|
const metadata = params?.additionalMetadata ?? {};
|
|
if (params?.additionalMetadata) {
|
|
for (const [key, value = ''] of Object.entries(metadata)) {
|
|
if (value) {
|
|
formData.append(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isAgentsEndpoint(endpoint)) {
|
|
if (!agent_id) {
|
|
formData.append('message_file', 'true');
|
|
}
|
|
if (toolResource != null) {
|
|
formData.append('tool_resource', toolResource);
|
|
}
|
|
if (conversation?.agent_id != null && formData.get('agent_id') == null) {
|
|
formData.append('agent_id', conversation.agent_id);
|
|
}
|
|
}
|
|
|
|
if (!isAssistantsEndpoint(endpoint)) {
|
|
uploadFile.mutate(formData);
|
|
return;
|
|
}
|
|
|
|
const convoModel = conversation?.model ?? '';
|
|
const convoAssistantId = conversation?.assistant_id ?? '';
|
|
|
|
if (!assistant_id) {
|
|
formData.append('message_file', 'true');
|
|
}
|
|
|
|
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
|
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
|
|
|
|
if (!assistant_id && convoAssistantId) {
|
|
formData.append('version', version);
|
|
formData.append('model', convoModel);
|
|
formData.append('assistant_id', convoAssistantId);
|
|
}
|
|
|
|
const formVersion = (formData.get('version') ?? '') as string;
|
|
if (!formVersion) {
|
|
formData.append('version', version);
|
|
}
|
|
|
|
const formModel = (formData.get('model') ?? '') as string;
|
|
if (!formModel) {
|
|
formData.append('model', convoModel);
|
|
}
|
|
|
|
uploadFile.mutate(formData);
|
|
};
|
|
|
|
const validateFiles = useCallback(
|
|
(fileList: File[]) => {
|
|
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 (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;
|
|
}
|
|
|
|
if (!checkType(originalFile.type, supportedMimeTypes)) {
|
|
console.log(originalFile);
|
|
setError('Currently, unsupported file type: ' + originalFile.type);
|
|
return false;
|
|
}
|
|
|
|
if (originalFile.size >= fileSizeLimit) {
|
|
setError(`File size exceeds ${fileSizeLimit / megabyte} MB.`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (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;
|
|
},
|
|
[files, fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes],
|
|
);
|
|
|
|
const loadImage = (extendedFile: ExtendedFile, preview: string) => {
|
|
const img = new Image();
|
|
img.onload = async () => {
|
|
extendedFile.width = img.width;
|
|
extendedFile.height = img.height;
|
|
extendedFile = {
|
|
...extendedFile,
|
|
progress: 0.6,
|
|
};
|
|
replaceFile(extendedFile);
|
|
|
|
await startUpload(extendedFile);
|
|
URL.revokeObjectURL(preview);
|
|
};
|
|
img.src = preview;
|
|
};
|
|
|
|
const handleFiles = async (_files: FileList | File[]) => {
|
|
abortControllerRef.current = new AbortController();
|
|
const fileList = Array.from(_files);
|
|
/* Validate files */
|
|
let filesAreValid: boolean;
|
|
try {
|
|
filesAreValid = validateFiles(fileList);
|
|
} catch (error) {
|
|
console.error('file validation error', error);
|
|
setError('com_error_files_validation');
|
|
return;
|
|
}
|
|
if (!filesAreValid) {
|
|
setFilesLoading(false);
|
|
return;
|
|
}
|
|
|
|
/* Process files */
|
|
for (const originalFile of fileList) {
|
|
const file_id = v4();
|
|
try {
|
|
const preview = URL.createObjectURL(originalFile);
|
|
const extendedFile: ExtendedFile = {
|
|
file_id,
|
|
file: originalFile,
|
|
type: originalFile.type,
|
|
preview,
|
|
progress: 0.2,
|
|
size: originalFile.size,
|
|
};
|
|
|
|
addFile(extendedFile);
|
|
|
|
if (originalFile.type.split('/')[0] === 'image') {
|
|
loadImage(extendedFile, preview);
|
|
continue;
|
|
}
|
|
|
|
await startUpload(extendedFile);
|
|
} catch (error) {
|
|
deleteFileById(file_id);
|
|
console.log('file handling error', error);
|
|
setError('com_error_files_process');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
event.stopPropagation();
|
|
if (event.target.files) {
|
|
setFilesLoading(true);
|
|
handleFiles(event.target.files);
|
|
// reset the input
|
|
event.target.value = '';
|
|
}
|
|
};
|
|
|
|
const abortUpload = () => {
|
|
if (abortControllerRef.current) {
|
|
logger.log('files', 'Aborting upload');
|
|
abortControllerRef.current.abort('User aborted upload');
|
|
abortControllerRef.current = null;
|
|
}
|
|
};
|
|
|
|
return {
|
|
handleFileChange,
|
|
setToolResource,
|
|
handleFiles,
|
|
abortUpload,
|
|
setFiles,
|
|
files,
|
|
};
|
|
};
|
|
|
|
export default useFileHandling;
|