mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-24 12:20:14 +01:00
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import { cloneDeep } from 'lodash';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Constants,
|
|
QueryKeys,
|
|
ContentTypes,
|
|
EModelEndpoint,
|
|
isAgentsEndpoint,
|
|
parseCompactConvo,
|
|
replaceSpecialVars,
|
|
isAssistantsEndpoint,
|
|
} from 'librechat-data-provider';
|
|
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
|
|
import type {
|
|
TMessage,
|
|
TSubmission,
|
|
TConversation,
|
|
TEndpointOption,
|
|
TEndpointsConfig,
|
|
EndpointSchemaKey,
|
|
} from 'librechat-data-provider';
|
|
import type { SetterOrUpdater } from 'recoil';
|
|
import type { TAskFunction, ExtendedFile } from '~/common';
|
|
import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
|
|
import useGetSender from '~/hooks/Conversations/useGetSender';
|
|
import store, { useGetEphemeralAgent } from '~/store';
|
|
import { getEndpointField, logger } from '~/utils';
|
|
import useUserKey from '~/hooks/Input/useUserKey';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuthContext } from '~/hooks';
|
|
|
|
const logChatRequest = (request: Record<string, unknown>) => {
|
|
logger.log('=====================================\nAsk function called with:');
|
|
logger.dir(request);
|
|
logger.log('=====================================');
|
|
};
|
|
|
|
export default function useChatFunctions({
|
|
index = 0,
|
|
files,
|
|
setFiles,
|
|
getMessages,
|
|
setMessages,
|
|
isSubmitting,
|
|
latestMessage,
|
|
setSubmission,
|
|
setLatestMessage,
|
|
conversation: immutableConversation,
|
|
}: {
|
|
index?: number;
|
|
isSubmitting: boolean;
|
|
paramId?: string | undefined;
|
|
conversation: TConversation | null;
|
|
latestMessage: TMessage | null;
|
|
getMessages: () => TMessage[] | undefined;
|
|
setMessages: (messages: TMessage[]) => void;
|
|
files?: Map<string, ExtendedFile>;
|
|
setFiles?: SetterOrUpdater<Map<string, ExtendedFile>>;
|
|
setSubmission: SetterOrUpdater<TSubmission | null>;
|
|
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
|
}) {
|
|
const navigate = useNavigate();
|
|
const getSender = useGetSender();
|
|
const { user } = useAuthContext();
|
|
const queryClient = useQueryClient();
|
|
const setFilesToDelete = useSetFilesToDelete();
|
|
const getEphemeralAgent = useGetEphemeralAgent();
|
|
const isTemporary = useRecoilValue(store.isTemporary);
|
|
const { getExpiry } = useUserKey(immutableConversation?.endpoint ?? '');
|
|
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
|
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
|
|
|
const ask: TAskFunction = (
|
|
{
|
|
text,
|
|
overrideConvoId,
|
|
overrideUserMessageId,
|
|
parentMessageId = null,
|
|
conversationId = null,
|
|
messageId = null,
|
|
toolResources,
|
|
},
|
|
{
|
|
editedContent = null,
|
|
editedMessageId = null,
|
|
isRegenerate = false,
|
|
isContinued = false,
|
|
isEdited = false,
|
|
overrideMessages,
|
|
overrideFiles,
|
|
} = {},
|
|
) => {
|
|
setShowStopButton(false);
|
|
resetLatestMultiMessage();
|
|
if (!!isSubmitting || text === '') {
|
|
return;
|
|
}
|
|
|
|
const conversation = cloneDeep(immutableConversation);
|
|
|
|
const endpoint = conversation?.endpoint;
|
|
if (endpoint === null) {
|
|
console.error('No endpoint available');
|
|
return;
|
|
}
|
|
|
|
conversationId = conversationId ?? conversation?.conversationId ?? null;
|
|
if (conversationId == 'search') {
|
|
console.error('cannot send any message under search view!');
|
|
return;
|
|
}
|
|
|
|
if (isContinued && !latestMessage) {
|
|
console.error('cannot continue AI message without latestMessage!');
|
|
return;
|
|
}
|
|
|
|
const ephemeralAgent = getEphemeralAgent(conversationId ?? Constants.NEW_CONVO);
|
|
const isEditOrContinue = isEdited || isContinued;
|
|
|
|
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
|
|
|
if (conversation?.promptPrefix) {
|
|
conversation.promptPrefix = replaceSpecialVars({
|
|
text: conversation.promptPrefix,
|
|
user,
|
|
});
|
|
}
|
|
|
|
// construct the query message
|
|
// this is not a real messageId, it is used as placeholder before real messageId returned
|
|
text = text.trim();
|
|
const intermediateId = overrideUserMessageId ?? v4();
|
|
parentMessageId = parentMessageId ?? latestMessage?.messageId ?? Constants.NO_PARENT;
|
|
|
|
logChatRequest({
|
|
index,
|
|
conversation,
|
|
latestMessage,
|
|
conversationId,
|
|
intermediateId,
|
|
parentMessageId,
|
|
currentMessages,
|
|
});
|
|
|
|
if (conversationId == Constants.NEW_CONVO) {
|
|
parentMessageId = Constants.NO_PARENT;
|
|
currentMessages = [];
|
|
conversationId = null;
|
|
navigate('/c/new', { state: { focusChat: true } });
|
|
}
|
|
|
|
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;
|
|
/**
|
|
* If the user regenerated or resubmitted the message, the current parent is technically
|
|
* the latest user message, which is passed into `ask`; otherwise, we can rely on the
|
|
* latestMessage to find the parent.
|
|
*/
|
|
const targetParentMessage = currentMessages.find(
|
|
(msg) => msg.messageId === targetParentMessageId,
|
|
);
|
|
|
|
let thread_id = targetParentMessage?.thread_id ?? latestMessage?.thread_id;
|
|
if (thread_id == null) {
|
|
thread_id = currentMessages.find((message) => message.thread_id)?.thread_id;
|
|
}
|
|
|
|
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
|
|
|
/** This becomes part of the `endpointOption` */
|
|
const convo = parseCompactConvo({
|
|
endpoint: endpoint as EndpointSchemaKey,
|
|
endpointType: endpointType as EndpointSchemaKey,
|
|
conversation: conversation ?? {},
|
|
});
|
|
|
|
const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {};
|
|
const endpointOption = Object.assign(
|
|
{
|
|
endpoint,
|
|
endpointType,
|
|
overrideConvoId,
|
|
overrideUserMessageId,
|
|
},
|
|
convo,
|
|
) as TEndpointOption;
|
|
if (endpoint !== EModelEndpoint.agents) {
|
|
endpointOption.key = getExpiry();
|
|
endpointOption.thread_id = thread_id;
|
|
endpointOption.modelDisplayLabel = modelDisplayLabel;
|
|
} else {
|
|
endpointOption.key = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
}
|
|
const responseSender = getSender({ model: conversation?.model, ...endpointOption });
|
|
|
|
const currentMsg: TMessage = {
|
|
text,
|
|
sender: 'User',
|
|
clientTimestamp: new Date().toLocaleString('sv').replace(' ', 'T'),
|
|
isCreatedByUser: true,
|
|
parentMessageId,
|
|
conversationId,
|
|
messageId: isContinued && messageId != null && messageId ? messageId : intermediateId,
|
|
thread_id,
|
|
error: false,
|
|
...(toolResources && { tool_resources: toolResources }),
|
|
};
|
|
|
|
console.log('ask() currentMsg before files processing:', {
|
|
text: currentMsg.text?.substring(0, 100) + '...',
|
|
tool_resources: currentMsg.tool_resources,
|
|
hasFiles: files?.size > 0,
|
|
filesSize: files?.size,
|
|
});
|
|
|
|
const submissionFiles = overrideFiles ?? targetParentMessage?.files;
|
|
const reuseFiles =
|
|
(isRegenerate || (overrideFiles != null && overrideFiles.length)) &&
|
|
submissionFiles &&
|
|
submissionFiles.length > 0;
|
|
|
|
console.log('ask() files processing:', {
|
|
overrideFiles,
|
|
hasOverrideFiles: !!overrideFiles?.length,
|
|
submissionFiles,
|
|
hasSubmissionFiles: !!submissionFiles?.length,
|
|
reuseFiles,
|
|
chatFilesSize: files?.size,
|
|
});
|
|
|
|
if (setFiles && reuseFiles === true) {
|
|
currentMsg.files = submissionFiles;
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
} else if (setFiles && files && files.size > 0) {
|
|
const chatFiles = Array.from(files.values()).map((file) => ({
|
|
file_id: file.file_id,
|
|
filepath: file.filepath,
|
|
type: file.type ?? '', // Ensure type is not undefined
|
|
height: file.height,
|
|
width: file.width,
|
|
}));
|
|
currentMsg.files = chatFiles;
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
}
|
|
|
|
console.log('ask() currentMsg after files processing:', {
|
|
text: currentMsg.text?.substring(0, 100) + '...',
|
|
tool_resources: currentMsg.tool_resources,
|
|
files: currentMsg.files,
|
|
hasFiles: !!currentMsg.files?.length,
|
|
});
|
|
|
|
const responseMessageId =
|
|
editedMessageId ??
|
|
(latestMessage?.messageId && isRegenerate
|
|
? latestMessage.messageId.replace(/_+$/, '') + '_'
|
|
: null) ??
|
|
null;
|
|
const initialResponseId =
|
|
responseMessageId ?? `${isRegenerate ? messageId : intermediateId}`.replace(/_+$/, '') + '_';
|
|
|
|
const initialResponse: TMessage = {
|
|
sender: responseSender,
|
|
text: '',
|
|
endpoint: endpoint ?? '',
|
|
parentMessageId: isRegenerate ? messageId : intermediateId,
|
|
messageId: initialResponseId,
|
|
thread_id,
|
|
conversationId,
|
|
unfinished: false,
|
|
isCreatedByUser: false,
|
|
iconURL: convo?.iconURL,
|
|
model: convo?.model,
|
|
error: false,
|
|
};
|
|
|
|
if (isAssistantsEndpoint(endpoint)) {
|
|
initialResponse.model = conversation?.assistant_id ?? '';
|
|
initialResponse.text = '';
|
|
initialResponse.content = [
|
|
{
|
|
type: ContentTypes.TEXT,
|
|
[ContentTypes.TEXT]: {
|
|
value: '',
|
|
},
|
|
},
|
|
];
|
|
} else if (endpoint != null) {
|
|
initialResponse.model = isAgentsEndpoint(endpoint)
|
|
? (conversation?.agent_id ?? '')
|
|
: (conversation?.model ?? '');
|
|
initialResponse.text = '';
|
|
|
|
if (editedContent && latestMessage?.content) {
|
|
initialResponse.content = cloneDeep(latestMessage.content);
|
|
const { index, type, ...part } = editedContent;
|
|
if (initialResponse.content && index >= 0 && index < initialResponse.content.length) {
|
|
const contentPart = initialResponse.content[index];
|
|
if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) {
|
|
contentPart[ContentTypes.THINK] = part[ContentTypes.THINK];
|
|
} else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) {
|
|
contentPart[ContentTypes.TEXT] = part[ContentTypes.TEXT];
|
|
}
|
|
}
|
|
} else {
|
|
initialResponse.content = [
|
|
{
|
|
type: ContentTypes.TEXT,
|
|
[ContentTypes.TEXT]: {
|
|
value: '',
|
|
},
|
|
},
|
|
];
|
|
}
|
|
setShowStopButton(true);
|
|
}
|
|
|
|
if (isContinued) {
|
|
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
|
|
}
|
|
|
|
logger.log('message_state', initialResponse);
|
|
const submission: TSubmission = {
|
|
conversation: {
|
|
...conversation,
|
|
conversationId,
|
|
},
|
|
endpointOption,
|
|
userMessage: {
|
|
...currentMsg,
|
|
responseMessageId,
|
|
overrideParentMessageId: isRegenerate ? messageId : null,
|
|
},
|
|
messages: currentMessages,
|
|
isEdited: isEditOrContinue,
|
|
isContinued,
|
|
isRegenerate,
|
|
initialResponse,
|
|
isTemporary,
|
|
ephemeralAgent,
|
|
editedContent,
|
|
};
|
|
|
|
if (isRegenerate) {
|
|
setMessages([...submission.messages, initialResponse]);
|
|
} else {
|
|
setMessages([...submission.messages, currentMsg, initialResponse]);
|
|
}
|
|
if (index === 0 && setLatestMessage) {
|
|
setLatestMessage(initialResponse);
|
|
}
|
|
|
|
setSubmission(submission);
|
|
logger.dir('message_stream', submission, { depth: null });
|
|
};
|
|
|
|
const regenerate = ({ parentMessageId }) => {
|
|
const messages = getMessages();
|
|
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
|
|
|
|
if (parentMessage && parentMessage.isCreatedByUser) {
|
|
ask({ ...parentMessage }, { isRegenerate: true });
|
|
} else {
|
|
console.error(
|
|
'Failed to regenerate the message: parentMessage not found or not created by user.',
|
|
);
|
|
}
|
|
};
|
|
|
|
return {
|
|
ask,
|
|
regenerate,
|
|
};
|
|
}
|