mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00

* wip: OpenAI Image Generation Tool with customizable options * WIP: First pass OpenAI Image Generation Tool and integrate into existing tools * 🔀 fix: Comment out unused validation for image generation tool parameters * 🔀 refactor: Update primeResources function parameters for better destructuring * feat: Add image_edit resource to EToolResources and update AgentToolResources interface * feat: Enhance file retrieval with tool resource filtering for image editing * refactor: add OpenAI Image Tools for generation and editing, refactor related components, pass current request image attachments as tool resources for editing * refactor: Remove commented-out code and clean up API key retrieval in createOpenAIImageTools function * fix: show message attachments in shared links * fix: Correct parent message retrieval logic for regenerated messages in useChatFunctions * fix: Update primeResources to utilize requestFileSet for image file processing * refactor: Improve description for image generation tool and clarify usage conditions, only provide edit tool if there are images available to edit * chore: Update OpenAI Image Tools icon to use local asset * refactor: Update image generation tool description and logic to prioritize editing tool when files are uploaded * refactor: Enhance image tool descriptions to clarify usage conditions and note potential unavailability of uploaded images * refactor: Update useAttachmentHandler to accept queryClient to update query cache with newly created file * refactor: Add customizable descriptions and prompts for OpenAI image generation and editing tools * chore: Update comments to use JSDoc style for better clarity and consistency * refactor: Rename config variable to clientConfig for clarity and update signal handling in image generation * refactor: Update axios request configuration to include derived signal and baseURL for improved request handling * refactor: Update baseURL environment variable for OpenAI image generation tool configuration * refactor: Enhance axios request configuration with conditional headers and improved clientConfig setup * chore: Update comments for clarity and remove unnecessary lines in OpenAI image tools * refactor: Update description for image generation without files to clarify user instructions * refactor: Simplify target parent message logic for regeneration and resubmission cases * chore: Remove backticks from error messages in image generation and editing functions * refactor: Rename toolResources to toolResourceSet for clarity in file retrieval functions * chore: Remove redundant comments and clean up TODOs in OpenAI image tools * refactor: Rename fileStrategy to appFileStrategy for clarity and improve error handling in image processing * chore: Update react-resizable-panels to version 2.1.8 in package.json and package-lock.json * chore: Ensure required validation for logs and Code of Conduct agreement in bug report template * fix: Update ArtifactPreview to use startupConfig and currentCode from memoized props to prevent unnecessary re-renders * fix: improve robustness of `save & submit` when used from a user-message with existing attachments * fix: add null check for artifact index in CodeEditor to prevent errors, trigger re-render on artifact ID change * fix: standardize default values for artifact properties in Artifact component, avoiding prematurely setting an "empty/default" artifact * fix: reset current artifact ID before setting a new one in ArtifactButton to ensure correct state management * chore: rename `setArtifactId` variable to `setCurrentArtifactId` for consistency * chore: update type annotations in File and S3 CRUD functions for consistency * refactor: improve image handling in OpenAI tools by using image_id references and enhance tool context for image editing * fix: update image_ids schema in image_edit_oai to enforce presence and provide clear guidelines for usage * fix: enhance file fetching logic to ensure user-specific and dimension-validated results * chore: add details on image generation and editing capabilities with various models
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Constants,
|
|
QueryKeys,
|
|
ContentTypes,
|
|
EModelEndpoint,
|
|
parseCompactConvo,
|
|
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 { getArtifactsMode } from '~/utils/artifacts';
|
|
import { getEndpointField, logger } from '~/utils';
|
|
import useUserKey from '~/hooks/Input/useUserKey';
|
|
|
|
const logChatRequest = (request: Record<string, unknown>) => {
|
|
logger.log('=====================================\nAsk function called with:');
|
|
logger.dir(request);
|
|
logger.log('=====================================');
|
|
};
|
|
|
|
const usesContentStream = (endpoint: EModelEndpoint | undefined, endpointType?: string) => {
|
|
if (endpointType === EModelEndpoint.custom) {
|
|
return true;
|
|
}
|
|
if (endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI) {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
export default function useChatFunctions({
|
|
index = 0,
|
|
files,
|
|
setFiles,
|
|
getMessages,
|
|
setMessages,
|
|
isSubmitting,
|
|
conversation,
|
|
latestMessage,
|
|
setSubmission,
|
|
setLatestMessage,
|
|
}: {
|
|
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 getEphemeralAgent = useGetEphemeralAgent();
|
|
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
|
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
|
const customPromptMode = useRecoilValue(store.customPromptMode);
|
|
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
|
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
|
const setFilesToDelete = useSetFilesToDelete();
|
|
const getSender = useGetSender();
|
|
const isTemporary = useRecoilValue(store.isTemporary);
|
|
|
|
const queryClient = useQueryClient();
|
|
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
|
|
|
const ask: TAskFunction = (
|
|
{
|
|
text,
|
|
overrideConvoId,
|
|
overrideUserMessageId,
|
|
parentMessageId = null,
|
|
conversationId = null,
|
|
messageId = null,
|
|
},
|
|
{
|
|
editedText = null,
|
|
editedMessageId = null,
|
|
isResubmission = false,
|
|
isRegenerate = false,
|
|
isContinued = false,
|
|
isEdited = false,
|
|
overrideMessages,
|
|
overrideFiles,
|
|
} = {},
|
|
) => {
|
|
setShowStopButton(false);
|
|
resetLatestMultiMessage();
|
|
if (!!isSubmitting || text === '') {
|
|
return;
|
|
}
|
|
|
|
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() ?? [];
|
|
|
|
// 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;
|
|
}
|
|
|
|
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,
|
|
artifacts:
|
|
endpoint !== EModelEndpoint.agents
|
|
? getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode })
|
|
: undefined,
|
|
},
|
|
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,
|
|
};
|
|
|
|
const submissionFiles = overrideFiles ?? targetParentMessage?.files;
|
|
const reuseFiles =
|
|
(isRegenerate || (overrideFiles != null && overrideFiles.length)) &&
|
|
submissionFiles &&
|
|
submissionFiles.length > 0;
|
|
|
|
if (setFiles && reuseFiles === true) {
|
|
currentMsg.files = [...submissionFiles];
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
} else if (setFiles && files && files.size > 0) {
|
|
currentMsg.files = 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,
|
|
}));
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
}
|
|
|
|
const generation = editedText ?? latestMessage?.text ?? '';
|
|
const responseText = isEditOrContinue ? generation : '';
|
|
|
|
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
|
|
const initialResponse: TMessage = {
|
|
sender: responseSender,
|
|
text: responseText,
|
|
endpoint: endpoint ?? '',
|
|
parentMessageId: isRegenerate ? messageId : intermediateId,
|
|
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
|
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: responseText,
|
|
},
|
|
},
|
|
];
|
|
} else if (endpoint === EModelEndpoint.agents) {
|
|
initialResponse.model = conversation?.agent_id ?? '';
|
|
initialResponse.text = '';
|
|
initialResponse.content = [
|
|
{
|
|
type: ContentTypes.TEXT,
|
|
[ContentTypes.TEXT]: {
|
|
value: responseText,
|
|
},
|
|
},
|
|
];
|
|
setShowStopButton(true);
|
|
} else if (usesContentStream(endpoint, endpointType)) {
|
|
initialResponse.text = '';
|
|
initialResponse.content = [
|
|
{
|
|
type: ContentTypes.TEXT,
|
|
[ContentTypes.TEXT]: {
|
|
value: responseText,
|
|
},
|
|
},
|
|
];
|
|
setShowStopButton(true);
|
|
} else {
|
|
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,
|
|
generation,
|
|
responseMessageId,
|
|
overrideParentMessageId: isRegenerate ? messageId : null,
|
|
},
|
|
messages: currentMessages,
|
|
isEdited: isEditOrContinue,
|
|
isContinued,
|
|
isRegenerate,
|
|
isResubmission,
|
|
initialResponse,
|
|
isTemporary,
|
|
ephemeralAgent,
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|