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

* feat: assistant name/icon in Landing & Header * feat: assistname in textarea placeholder, and use `Assistant` as default name * feat: display non-image files in user messages * fix: only render files if files.length is > 0 * refactor(config -> file-config): move file related configuration values to separate module, add excel types * chore: spreadsheet file rendering * fix(Landing): dark mode style for Assistant Name * refactor: move progress incrementing to own hook, start smaller, cap near limit \(1\) * refactor(useContentHandler): add empty Text part if last part was completed tool or image * chore: add accordion trigger border styling for dark mode * feat: Assistant Builder model selection * chore: use Spinner when Assistant is mutating * fix(get/assistants): return correct response object `AssistantListResponse` * refactor(Spinner): pass size as prop * refactor: make assistant crud mutations optimistic, add types for options * chore: remove assistants route and view * chore: move assistant builder components to separate directory * feat(ContextButton): delete Assistant via context button/dialog, add localization * refactor: conditionally show use and context menu buttons, add localization for create assistant * feat: save side panel states to localStorage * style(SidePanel): improve avatar menu and assistant select styling for dark mode * refactor: make NavToggle reusable for either side (left or right), add SidePanel Toggle with ability to close it completely * fix: resize handle and navToggle behavior * fix(/avatar/:assistant_id): await `deleteFile` and assign unique name to uploaded image * WIP: file UI components from PR #576 * refactor(OpenAIMinimalIcon): pass className * feat: formatDate helper fn * feat: DataTableColumnHeader * feat: add row selection, formatted row values, number of rows selected * WIP: add files to Side panel temporarily * feat: `LB_QueueAsyncCall`: Leaky Bucket queue for external APIs, use in `processDeleteRequest` * fix(TFile): correct `source` type with `FileSources` * fix(useFileHandling): use `continue` instead of return when iterating multiple files, add file type to extendedFile * chore: add generic setter type * refactor(processDeleteRequest): settle promises to prevent rejections from processing deletions, log errors * feat: `useFileDeletion` to reuse file deletion logic * refactor(useFileDeletion): make `setFiles` an optional param and use object as param * feat: useDeleteFilesFromTable * feat: use real `files` data and add deletion action to data table * fix(Table): make headers sticky * feat: add dynamic filtering for columns; only show to user Host or OpenAI storage type * style(DropdownMenu): replace `slate` with `gray` * style(DataTable): apply dark mode themes and other misc styling * style(Columns): add color to OpenAI Storage option * refactor(FileContainer): make file preview reusable * refactor(Images): make image preview reusable * refactor(FilePreview): make file prop optional for FileIcon and FilePreview, fix relative style * feat(Columns): add file/image previews, set a minimum size to show for file size in bytes * WIP: File Panel with real files and formatted * feat: open files dialog from panel * style: file data table mobile and general column styling fixes * refactor(api/files): return files sorted by the most recently updated * refactor: provide fileMap through context to prevent re-selecting files to map in different areas; remove unused imports commented out in PanelColumns * refactor(ExtendFile): make File type optional, add `attached` to prevent attached files from being deleted on remove, make Message.files a partial TFile type * feat: attach files through file panel * refactor(useFileHandling): move files to the start of cache list when uploaded * refactor(useDeleteFilesMutation): delete files from cache when successfully deleted from server * fix(FileRow): handle possible edge case of duplication due to attaching recently uploaded file * style(SidePanel): make resize grip border transparent, remove unnecessary styling on close sidepanel button * feat: action utilities and tests * refactor(actions): add `ValidationResult` type and change wording for no server URL found * refactor(actions): check for empty server URL * fix(data-provider): revert tsconfig to fix type issue resolution * feat(client): first pass of actions input for assistants * refactor(FunctionSignature): change method to output object instead of string * refactor(models/Assistant): add actions field to schema, use searchParams object for methods, and add `getAssistant` * feat: post actions input first pass - create new Action document - add actions to Assistant DB document - create /action/:assistant_id POST route - pass more props down from PanelSwitcher, derive assistant_id from switcher - move privacy policy to ActionInput - reset data on input change/validation - add `useUpdateAction` - conform FunctionSignature type to FunctionTool - add action, assistant doc, update hook related types * refactor: optimize assistant/actions relationship - past domain in metadata as hostname and not a URL - include domain in tool name - add `getActions` for actions retrieval by user - add `getAssistants` for assistant docs retrieval by user - add `assistant_id` to Action schema - move actions to own module as a subroute to `api/assistants` - add `useGetActionsQuery` and `useGetAssistantDocsQuery` hooks - fix Action type def * feat: show assistant actions in assistant builder * feat: switch to actions on action click, editing action styling * fix: add Assistant state for builder panel to allow immediate selection of newly created assistants as well as retaining the current assistant when switching to a different panel within the builder * refactor(SidePanel/NavToggle): offset less from right when SidePanel is completely collapsed * chore: rename `processActions` -> `processRequiredActions` * chore: rename Assistant API Action to RequiredAction * refactor(actions): avoid nesting actual API params under generic `requestBody` to optimize LLM token usage * fix(handleTools): avoid calling `validTool` if not defined, add optional param to skip the loading of specs, which throws an error in the context of assistants * WIP: working first pass of toolCalls generated from openapi specs * WIP: first pass ToolCall styling * feat: programmatic iv encryption/decryption helpers * fix: correct ActionAuth types/enums, and define type for AuthForm * feat: encryption/decryption helpers for Action AuthMetadata * refactor(getActions): remove sensitive fields from query response * refactor(POST/actions): encrypt and remove sensitive fields from mutation response * fix(ActionService): change ESM import to CJS * feat: frontend auth handling for actions + optimistic update on action update/creation * refactor(actions): use the correct variables and types for setAuth method * refactor: POST /:assistant_id action can now handle updating an existing action, add `saved_auth_fields` to determine when user explicitly saves new auth creds. only send auth metadata if user explicitly saved fields * refactor(createActionTool): catch errors and send back meaningful error message, add flag to `getActions` to determine whether to retrieve sensitive values or not * refactor(ToolService): add `action` property to ToolCall PartMetadata to determine if the tool call was an action, fix parsing function name issue with actionDelimiter * fix(ActionRequest): use URL class to correctly join endpoint parts for `execute` call * feat: delete assistant actions * refactor: conditionally show Available actions * refactor: show `retrieval` and `code_interpreter` as Capabilities, swap `Switch` for `Checkbox` * chore: remove shadow-stroke from messages * WIP: first pass of Assistants Knowledge attachments * refactor: remove AssistantsProvider in favor of FormProvider, fix selectedAssistant re-render bug, map Assistant file_ids to files via fileMap, initialize Knowledge component with mapped files if any exist * fix: prevent deleting files on assistant file upload * chore: remove console.log * refactor(useUploadFileMutation): update files and assistants cache on upload * chore: disable oauth option as not supported yet * feat: cancel assistant runs * refactor: initialize OpenAI client with helper function, resolve all related circular dependencies * fix(DALL-E): initialization * fix(process): openai client initialization * fix: select an existing Assistant when the active one is deleted * chore: allow attaching files for assistant endpoint, send back relevant OpenAI error message when uploading, deconstruct openAI initialization correctly, add `message_file` to formData when a file is attached to the message but not the assistant * fix: add assistant_id on newConvo * fix(initializeClient): import fix * chore: swap setAssistant for setOption in useEffect * fix(DALL-E): add processFileURL to loadTools call * chore: add customConfig to debug logs * feat: delete threads on convo delete * chore: replace Assistants icon * chore: remove console.dir() in `abortRun` * feat(AssistantService): accumulate text values from run in openai.responseText * feat: titling for assistants endpoint * chore: move panel file components to appropriate directory, add file checks for attaching files, change icon for Attach Files * refactor: add localizations to tools, plugins, add condition for adding/remove user plugins so tool selections don't affect this value * chore: disable `import from url` action for now * chore: remove textMimeTypes from default fileConfig for now * fix: catch tool errors and send as outputs with error messages * fix: React warning about button as descendant of button * style: retrieval and cancelled icon * WIP: pass isSubmitting to Parts, use InProgressCall to display cancelled tool calls correctly, show domain/function name * fix(meilisearch): fix `postSaveHook` issue where indexing expects a mongo document, and join all text content parts for meili indexing * ci: fix dall-e tests * ci: fix client tests * fix: button types in actions panel * fix: plugin auth form persisting across tool selections * fix(ci): update AppService spec with `loadAndFormatTools` * fix(clearConvos): add id check earlier on * refactor(AssistantAvatar): set previewURL dynamically when emtadata.avatar changes * feat(assistants): addTitle cache setting * fix(useSSE): resolve rebase conflicts * fix: delete mutation * style(SidePanel): make grip visible on active and hover, invisible otherwise * ci: add data-provider tests to workflow, also update eslint/tsconfig to recognize specs, and add `text/csv` to fileConfig * fix: handle edge case where auth object is undefined, and log errors * refactor(actions): resolve schemas, add tests for resolving refs, import specs from separate file for tests * chore: remove comment * fix(ActionsInput): re-render bug when initializing states with action fields * fix(patch/assistant): filter undefined tools * chore: add logging for errors in assistants routes * fix(updateAssistant): map actions to functions to avoid overwriting * fix(actions): properly handle GET paths * fix(convos): unhandled delete thread exception * refactor(AssistantService): pass both thread_id and conversationId when sending intermediate assistant messages, remove `mapMessagesToSteps` from AssistantService * refactor(useSSE): replace all messages with runMessages and pass latestMessageId to abortRun; fix(checkMessageGaps): include tool calls when syncing messages * refactor(assistants/chat): invoke `createOnTextProgress` after thread creation * chore: add typing * style: sidepanel styling * style: action tool call domain styling * feat(assistants): default models, limit retrieval to certain models, add env variables to to env.example * feat: assistants api key in EndpointService * refactor: set assistant model to conversation on assistant switch * refactor: set assistant model to conversation on assistant select from panel * fix(retrieveAndProcessFile): catch attempt to download file with `assistant` purpose which is not allowed; add logging * feat: retrieval styling, handling, and logging * chore: rename ASSISTANTS_REVERSE_PROXY to ASSISTANTS_BASE_URL * feat: FileContext for file metadata * feat: context file mgmt and filtering * style(Select): hover/rounded changes * refactor: explicit conversation switch, endpoint dependent, through `useSelectAssistant`, which does not create new chat if current endpoint is assistant endpoint * fix(AssistantAvatar): make empty previewURL if no avatar present * refactor: side panel mobile styling * style: merge tool and action section, optimize mobile styling for action/tool buttons * fix: localStorage issues * fix(useSelectAssistant): invoke react query hook directly in select hook as Map was not being updated in time * style: light mode fixes * fix: prevent sidepanel nav styling from shifting layout up * refactor: change default layout (collapsed by default) * style: mobile optimization of DataTable * style: datatable * feat: client-side hide right-side panel * chore(useNewConvo): add partial typing for preset * fix(useSelectAssistant): pass correct model name by using template as preset * WIP: assistant presets * refactor(ToolService): add native solution for `TavilySearchResults` and log tool output errors * refactor: organize imports and use native TavilySearchResults * fix(TavilySearchResults): stringify result * fix(ToolCall): show tool call outputs when not an action * chore: rename Prompt Prefix to custom instructions (in user facing text only) * refactor(EditPresetDialog): Optimize setting title by debouncing, reset preset on dialog close to avoid state mixture * feat: add `presetOverride` to overwrite active conversation settings when saving a Preset (relevant for client side updates only) * feat: Assistant preset settings (client-side) * fix(Switcher): only set assistant_id and model if current endpoint is Assistants * feat: use `useDebouncedInput` for updating conversation settings, starting with EditPresetDialog title setting and Assistant instructions setting * feat(Assistants): add instructions field to settings * feat(chat/assistants): pass conversation settings to run body * wip: begin localization and only allow actions if the assistant is created * refactor(AssistantsPanel): knowledge localization, allow tools on creation * feat: experimental: allow 'priming' values before assistant is created, that would normally require an assistant_id to be defined * chore: trim console logs and make more meaningful * chore: toast messages * fix(ci): date test * feat: create file when uploading Assistant Avatar * feat: file upload rate limiting from custom config with dynamic file route initialization * refactor: use file upload limiters on post routes only * refactor(fileConfig): add endpoints field for endpoint specific fileconfigs, add mergeConfig function, add tests * refactor: fileConfig route, dynamic multer instances used on all '/' and '/images' POST routes, data service and query hook * feat: supportedMimeTypesSchema, test for array of regex * feat: configurable file config limits * chore: clarify assistants file knowledge prereq. * chore(useTextarea): default to localized 'Assistant' if assistant name is empty * feat: configurable file limits and toggle file upload per endpoint * fix(useUploadFileMutation): prevent updating assistant.files cache if file upload is a message_file attachment * fix(AssistantSelect): set last selected assistant only when timeout successfully runs * refactor(queries): disable assistant queries if assistants endpoint is not enabled * chore(Switcher): add localization * chore: pluralize `assistant` for `EModelEndpoint key and value * feat: show/hide assistant UI components based on endpoint availability; librechat.yaml config for disabling builder section and setting polling/timeout intervals * fix(compactEndpointSchemas): use EModelEndpoint for schema access * feat(runAssistant): use configured values from `librechat.yaml` for `pollIntervalMs` and `timeout` * fix: naming issue * wip: revert landing * 🎉 happy birthday LibreChat (#1768) * happy birthday LibreChat * Refactor endpoint condition in Landing component * Update birthday message in Eng.tsx * fix(/config): avoid nesting ternaries * refactor(/config): check birthday --------- Co-authored-by: Danny Avila <messagedaniel@protonmail.com> * fix: landing * fix: landing * fix(useMessageHelpers): hardcoded check to use EModelEndpoint instead * fix(ci): convo test revert to main * fix(assistants/chat): fix issue where assistant_id was being saved as model for convo * chore: added logging, promises racing to prevent longer timeouts, explicit setting of maxRetries and timeouts, robust catching of invalid abortRun params * refactor: use recoil state for `showStopButton` and only show for assistants endpoint after syncing conversation data * refactor: optimize abortRun strategy using localStorage, refactor `abortConversation` to use async/await and await the result, refactor how the abortKey cache is set for runs * fix(checkMessageGaps): assign `assistant_id` to synced messages if defined; prevents UI from showing blank assistant for cancelled messages * refactor: re-order sequence of chat route, only allow aborting messages after run is created, cancel abortRun if there was a cancelling error (likely due already cancelled in chat route), and add extra logging * chore(typedefs): add httpAgent type to OpenAIClient * refactor: use custom implementation of retrieving run with axios to allow for timing out run query * fix(waitForRun): handle timed out run retrieval query * refactor: update preset conditions: - presets will retain settings when a different endpoint is selected; for existing convos, either when modular or is assistant switch - no longer use `navigateToConvo` on preset select * fix: temporary calculator hack as expects string input when invoked * fix: cancel abortRun only when cancelling error is a result of the run already being cancelled * chore: remove use of `fileMaxSizeMB` and total counterpart (redundant) * docs: custom config documentation update * docs: assistants api setup and dotenv, new custom config fields * refactor(Switcher): make Assistant switcher sticky in SidePanel * chore(useSSE): remove console log of data and message index * refactor(AssistantPanel): button styling and add secondary select button to bottom of panel * refactor(OpenAIClient): allow passing conversationId to RunManager through titleConvo and initializeLLM to properly record title context tokens used in cases where conversationId was not defined by the client * feat(assistants): token tracking for assistant runs * chore(spendTokens): improve logging * feat: support/exclude specific assistant Ids * chore: add update `librechat.example.yaml`, optimize `AppService` handling, new tests for `AppService`, optimize missing/outdate config logging * chore: mount docker logs to root of project * chore: condense axios errors * chore: bump vite * chore: vite hot reload fix using latest version * chore(getOpenAIModels): sort instruct models to the end of models list * fix(assistants): user provided key * fix(assistants): user provided key, invalidate more queries on revoke --------- Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
495 lines
16 KiB
JavaScript
495 lines
16 KiB
JavaScript
const { v4 } = require('uuid');
|
|
const {
|
|
EModelEndpoint,
|
|
Constants,
|
|
defaultOrderQuery,
|
|
ContentTypes,
|
|
} = require('librechat-data-provider');
|
|
const { recordMessage, getMessages } = require('~/models/Message');
|
|
const { saveConvo } = require('~/models/Conversation');
|
|
const spendTokens = require('~/models/spendTokens');
|
|
const { countTokens } = require('~/server/utils');
|
|
|
|
/**
|
|
* Initializes a new thread or adds messages to an existing thread.
|
|
*
|
|
* @param {Object} params - The parameters for initializing a thread.
|
|
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
|
* @param {Object} params.body - The body of the request.
|
|
* @param {ThreadMessage[]} params.body.messages - A list of messages to start the thread with.
|
|
* @param {Object} [params.body.metadata] - Optional metadata for the thread.
|
|
* @param {string} [params.thread_id] - Optional existing thread ID. If provided, a message will be added to this thread.
|
|
* @return {Promise<Thread>} A promise that resolves to the newly created thread object or the updated thread object.
|
|
*/
|
|
async function initThread({ openai, body, thread_id: _thread_id }) {
|
|
let thread = {};
|
|
const messages = [];
|
|
if (_thread_id) {
|
|
const message = await openai.beta.threads.messages.create(_thread_id, body.messages[0]);
|
|
messages.push(message);
|
|
} else {
|
|
thread = await openai.beta.threads.create(body);
|
|
}
|
|
|
|
const thread_id = _thread_id ?? thread.id;
|
|
return { messages, thread_id, ...thread };
|
|
}
|
|
|
|
/**
|
|
* Saves a user message to the DB in the Assistants endpoint format.
|
|
*
|
|
* @param {Object} params - The parameters of the user message
|
|
* @param {string} params.user - The user's ID.
|
|
* @param {string} params.text - The user's prompt.
|
|
* @param {string} params.messageId - The user message Id.
|
|
* @param {string} params.model - The model used by the assistant.
|
|
* @param {string} params.assistant_id - The current assistant Id.
|
|
* @param {string} params.thread_id - The thread Id.
|
|
* @param {string} params.conversationId - The message's conversationId
|
|
* @param {string} [params.parentMessageId] - Optional if initial message.
|
|
* Defaults to Constants.NO_PARENT.
|
|
* @param {string} [params.instructions] - Optional: from preset for `instructions` field.
|
|
* Overrides the instructions of the assistant.
|
|
* @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field.
|
|
* @param {import('librechat-data-provider').TFile[]} [params.files] - Optional. List of Attached File Objects.
|
|
* @param {string[]} [params.file_ids] - Optional. List of File IDs attached to the userMessage.
|
|
* @return {Promise<Run>} A promise that resolves to the created run object.
|
|
*/
|
|
async function saveUserMessage(params) {
|
|
const tokenCount = await countTokens(params.text);
|
|
|
|
// todo: do this on the frontend
|
|
// const { file_ids = [] } = params;
|
|
// let content;
|
|
// if (file_ids.length) {
|
|
// content = [
|
|
// {
|
|
// value: params.text,
|
|
// },
|
|
// ...(
|
|
// file_ids
|
|
// .filter(f => f)
|
|
// .map((file_id) => ({
|
|
// file_id,
|
|
// }))
|
|
// ),
|
|
// ];
|
|
// }
|
|
|
|
const userMessage = {
|
|
user: params.user,
|
|
endpoint: EModelEndpoint.assistants,
|
|
messageId: params.messageId,
|
|
conversationId: params.conversationId,
|
|
parentMessageId: params.parentMessageId ?? Constants.NO_PARENT,
|
|
/* For messages, use the assistant_id instead of model */
|
|
model: params.assistant_id,
|
|
thread_id: params.thread_id,
|
|
sender: 'User',
|
|
text: params.text,
|
|
isCreatedByUser: true,
|
|
tokenCount,
|
|
};
|
|
|
|
const convo = {
|
|
endpoint: EModelEndpoint.assistants,
|
|
conversationId: params.conversationId,
|
|
promptPrefix: params.promptPrefix,
|
|
instructions: params.instructions,
|
|
assistant_id: params.assistant_id,
|
|
model: params.model,
|
|
};
|
|
|
|
if (params.files?.length) {
|
|
userMessage.files = params.files.map(({ file_id }) => ({ file_id }));
|
|
convo.file_ids = params.file_ids;
|
|
}
|
|
|
|
const message = await recordMessage(userMessage);
|
|
await saveConvo(params.user, convo);
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Saves an Assistant message to the DB in the Assistants endpoint format.
|
|
*
|
|
* @param {Object} params - The parameters of the Assistant message
|
|
* @param {string} params.user - The user's ID.
|
|
* @param {string} params.messageId - The message Id.
|
|
* @param {string} params.assistant_id - The assistant Id.
|
|
* @param {string} params.thread_id - The thread Id.
|
|
* @param {string} params.model - The model used by the assistant.
|
|
* @param {ContentPart[]} params.content - The message content parts.
|
|
* @param {string} params.conversationId - The message's conversationId
|
|
* @param {string} params.parentMessageId - The latest user message that triggered this response.
|
|
* @param {string} [params.instructions] - Optional: from preset for `instructions` field.
|
|
* Overrides the instructions of the assistant.
|
|
* @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field.
|
|
* @return {Promise<Run>} A promise that resolves to the created run object.
|
|
*/
|
|
async function saveAssistantMessage(params) {
|
|
const text = params.content.reduce((acc, part) => {
|
|
if (!part.value) {
|
|
return acc;
|
|
}
|
|
|
|
return acc + ' ' + part.value;
|
|
}, '');
|
|
|
|
// const tokenCount = // TODO: need to count each content part
|
|
|
|
const message = await recordMessage({
|
|
user: params.user,
|
|
endpoint: EModelEndpoint.assistants,
|
|
messageId: params.messageId,
|
|
conversationId: params.conversationId,
|
|
parentMessageId: params.parentMessageId,
|
|
thread_id: params.thread_id,
|
|
/* For messages, use the assistant_id instead of model */
|
|
model: params.assistant_id,
|
|
content: params.content,
|
|
sender: 'Assistant',
|
|
isCreatedByUser: false,
|
|
text: text.trim(),
|
|
// tokenCount,
|
|
});
|
|
|
|
await saveConvo(params.user, {
|
|
endpoint: EModelEndpoint.assistants,
|
|
conversationId: params.conversationId,
|
|
promptPrefix: params.promptPrefix,
|
|
instructions: params.instructions,
|
|
assistant_id: params.assistant_id,
|
|
model: params.model,
|
|
});
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Records LibreChat messageId to all response messages' metadata
|
|
*
|
|
* @param {Object} params - The parameters for initializing a thread.
|
|
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
|
* @param {string} params.thread_id - Response thread ID.
|
|
* @param {string} params.messageId - The response `messageId` generated by LibreChat.
|
|
* @param {StepMessage[] | Message[]} params.messages - A list of messages to start the thread with.
|
|
* @return {Promise<ThreadMessage[]>} A promise that resolves to the updated messages
|
|
*/
|
|
async function addThreadMetadata({ openai, thread_id, messageId, messages }) {
|
|
const promises = [];
|
|
for (const message of messages) {
|
|
promises.push(
|
|
openai.beta.threads.messages.update(thread_id, message.id, {
|
|
metadata: {
|
|
messageId,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
return await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Synchronizes LibreChat messages to Thread Messages.
|
|
* Updates the LibreChat DB with any missing Thread Messages and
|
|
* updates the missing Thread Messages' metadata with their corresponding db messageId's.
|
|
*
|
|
* Also updates the existing conversation's file_ids with any new file_ids.
|
|
*
|
|
* @param {Object} params - The parameters for synchronizing messages.
|
|
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
|
* @param {TMessage[]} params.dbMessages - The LibreChat DB messages.
|
|
* @param {ThreadMessage[]} params.apiMessages - The thread messages from the API.
|
|
* @param {string} params.conversationId - The current conversation ID.
|
|
* @param {string} params.thread_id - The current thread ID.
|
|
* @param {string} [params.assistant_id] - The current assistant ID.
|
|
* @return {Promise<TMessage[]>} A promise that resolves to the updated messages
|
|
*/
|
|
async function syncMessages({
|
|
openai,
|
|
apiMessages,
|
|
dbMessages,
|
|
conversationId,
|
|
thread_id,
|
|
assistant_id,
|
|
}) {
|
|
let result = [];
|
|
let dbMessageMap = new Map(dbMessages.map((msg) => [msg.messageId, msg]));
|
|
|
|
const modifyPromises = [];
|
|
const recordPromises = [];
|
|
|
|
/**
|
|
*
|
|
* Modify API message and save newMessage to DB
|
|
*
|
|
* @param {Object} params - The parameters object
|
|
* @param {TMessage} params.dbMessage
|
|
* @param {dbMessage} params.apiMessage
|
|
*/
|
|
const processNewMessage = async ({ dbMessage, apiMessage }) => {
|
|
recordPromises.push(recordMessage({ ...dbMessage, user: openai.req.user.id }));
|
|
|
|
if (!apiMessage.id.includes('msg_')) {
|
|
return;
|
|
}
|
|
|
|
if (dbMessage.aggregateMessages?.length > 1) {
|
|
modifyPromises.push(
|
|
addThreadMetadata({
|
|
openai,
|
|
thread_id,
|
|
messageId: dbMessage.messageId,
|
|
messages: dbMessage.aggregateMessages,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
modifyPromises.push(
|
|
openai.beta.threads.messages.update(thread_id, apiMessage.id, {
|
|
metadata: {
|
|
messageId: dbMessage.messageId,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
let lastMessage = null;
|
|
|
|
for (let i = 0; i < apiMessages.length; i++) {
|
|
const apiMessage = apiMessages[i];
|
|
|
|
// Check if the message exists in the database based on metadata
|
|
const dbMessageId = apiMessage.metadata && apiMessage.metadata.messageId;
|
|
let dbMessage = dbMessageMap.get(dbMessageId);
|
|
|
|
if (dbMessage) {
|
|
// If message exists in DB, use its messageId and update parentMessageId
|
|
dbMessage.parentMessageId = lastMessage ? lastMessage.messageId : Constants.NO_PARENT;
|
|
lastMessage = dbMessage;
|
|
result.push(dbMessage);
|
|
continue;
|
|
}
|
|
|
|
if (apiMessage.role === 'assistant' && lastMessage && lastMessage.role === 'assistant') {
|
|
// Aggregate assistant messages
|
|
lastMessage.content = [...lastMessage.content, ...apiMessage.content];
|
|
lastMessage.files = [...(lastMessage.files ?? []), ...(apiMessage.files ?? [])];
|
|
lastMessage.aggregateMessages.push({ id: apiMessage.id });
|
|
} else {
|
|
// Handle new or missing message
|
|
const newMessage = {
|
|
thread_id,
|
|
conversationId,
|
|
messageId: v4(),
|
|
endpoint: EModelEndpoint.assistants,
|
|
parentMessageId: lastMessage ? lastMessage.messageId : Constants.NO_PARENT,
|
|
role: apiMessage.role,
|
|
isCreatedByUser: apiMessage.role === 'user',
|
|
// TODO: process generated files in content parts
|
|
content: apiMessage.content,
|
|
aggregateMessages: [{ id: apiMessage.id }],
|
|
model: apiMessage.role === 'user' ? null : apiMessage.assistant_id,
|
|
user: openai.req.user.id,
|
|
};
|
|
|
|
if (apiMessage.file_ids?.length) {
|
|
// TODO: retrieve file objects from API
|
|
newMessage.files = apiMessage.file_ids.map((file_id) => ({ file_id }));
|
|
}
|
|
|
|
/* Assign assistant_id if defined */
|
|
if (assistant_id && apiMessage.role === 'assistant' && !newMessage.model) {
|
|
apiMessage.model = assistant_id;
|
|
newMessage.model = assistant_id;
|
|
}
|
|
|
|
result.push(newMessage);
|
|
lastMessage = newMessage;
|
|
|
|
if (apiMessage.role === 'user') {
|
|
processNewMessage({ dbMessage: newMessage, apiMessage });
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const nextMessage = apiMessages[i + 1];
|
|
const processAssistant = !nextMessage || nextMessage.role === 'user';
|
|
|
|
if (apiMessage.role === 'assistant' && processAssistant) {
|
|
processNewMessage({ dbMessage: lastMessage, apiMessage });
|
|
}
|
|
}
|
|
|
|
const attached_file_ids = apiMessages.reduce((acc, msg) => {
|
|
if (msg.role === 'user' && msg.file_ids?.length) {
|
|
return [...acc, ...msg.file_ids];
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
|
|
await Promise.all(modifyPromises);
|
|
await Promise.all(recordPromises);
|
|
|
|
await saveConvo(openai.req.user.id, {
|
|
conversationId,
|
|
file_ids: attached_file_ids,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Maps messages to their corresponding steps. Steps with message creation will be paired with their messages,
|
|
* while steps without message creation will be returned as is.
|
|
*
|
|
* @param {RunStep[]} steps - An array of steps from the run.
|
|
* @param {Message[]} messages - An array of message objects.
|
|
* @returns {(StepMessage | RunStep)[]} An array where each element is either a step with its corresponding message (StepMessage) or a step without a message (RunStep).
|
|
*/
|
|
function mapMessagesToSteps(steps, messages) {
|
|
// Create a map of messages indexed by their IDs for efficient lookup
|
|
const messageMap = messages.reduce((acc, msg) => {
|
|
acc[msg.id] = msg;
|
|
return acc;
|
|
}, {});
|
|
|
|
// Map each step to its corresponding message, or return the step as is if no message ID is present
|
|
return steps
|
|
.sort((a, b) => a.created_at - b.created_at)
|
|
.map((step) => {
|
|
const messageId = step.step_details?.message_creation?.message_id;
|
|
|
|
if (messageId && messageMap[messageId]) {
|
|
return { step, message: messageMap[messageId] };
|
|
}
|
|
return step;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks for any missing messages; if missing,
|
|
* synchronizes LibreChat messages to Thread Messages
|
|
*
|
|
* @param {Object} params - The parameters for initializing a thread.
|
|
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
|
* @param {string} [params.latestMessageId] - Optional: The latest message ID from LibreChat.
|
|
* @param {string} params.thread_id - Response thread ID.
|
|
* @param {string} params.run_id - Response Run ID.
|
|
* @param {string} params.conversationId - LibreChat conversation ID.
|
|
* @return {Promise<TMessage[]>} A promise that resolves to the updated messages
|
|
*/
|
|
async function checkMessageGaps({ openai, latestMessageId, thread_id, run_id, conversationId }) {
|
|
const promises = [];
|
|
promises.push(openai.beta.threads.messages.list(thread_id, defaultOrderQuery));
|
|
promises.push(openai.beta.threads.runs.steps.list(thread_id, run_id));
|
|
/** @type {[{ data: ThreadMessage[] }, { data: RunStep[] }]} */
|
|
const [response, stepsResponse] = await Promise.all(promises);
|
|
|
|
const steps = mapMessagesToSteps(stepsResponse.data, response.data);
|
|
/** @type {ThreadMessage} */
|
|
const currentMessage = {
|
|
id: v4(),
|
|
content: [],
|
|
assistant_id: null,
|
|
created_at: Math.floor(new Date().getTime() / 1000),
|
|
object: 'thread.message',
|
|
role: 'assistant',
|
|
run_id,
|
|
thread_id,
|
|
metadata: {
|
|
messageId: latestMessageId,
|
|
},
|
|
};
|
|
|
|
for (const step of steps) {
|
|
if (!currentMessage.assistant_id && step.assistant_id) {
|
|
currentMessage.assistant_id = step.assistant_id;
|
|
}
|
|
if (step.message) {
|
|
currentMessage.id = step.message.id;
|
|
currentMessage.created_at = step.message.created_at;
|
|
currentMessage.content = currentMessage.content.concat(step.message.content);
|
|
} else if (step.step_details?.type === 'tool_calls' && step.step_details?.tool_calls?.length) {
|
|
currentMessage.content = currentMessage.content.concat(
|
|
step.step_details?.tool_calls.map((toolCall) => ({
|
|
[ContentTypes.TOOL_CALL]: {
|
|
...toolCall,
|
|
progress: 2,
|
|
},
|
|
type: ContentTypes.TOOL_CALL,
|
|
})),
|
|
);
|
|
}
|
|
}
|
|
|
|
let addedCurrentMessage = false;
|
|
const apiMessages = response.data.map((msg) => {
|
|
if (msg.id === currentMessage.id) {
|
|
addedCurrentMessage = true;
|
|
return currentMessage;
|
|
}
|
|
return msg;
|
|
});
|
|
|
|
if (!addedCurrentMessage) {
|
|
apiMessages.push(currentMessage);
|
|
}
|
|
|
|
const dbMessages = await getMessages({ conversationId });
|
|
const assistant_id = dbMessages?.[0]?.model;
|
|
|
|
const syncedMessages = await syncMessages({
|
|
openai,
|
|
dbMessages,
|
|
apiMessages,
|
|
thread_id,
|
|
conversationId,
|
|
assistant_id,
|
|
});
|
|
|
|
return Object.values(
|
|
[...dbMessages, ...syncedMessages].reduce(
|
|
(acc, message) => ({ ...acc, [message.messageId]: message }),
|
|
{},
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Records token usage for a given completion request.
|
|
*
|
|
* @param {Object} params - The parameters for initializing a thread.
|
|
* @param {number} params.prompt_tokens - The number of prompt tokens used.
|
|
* @param {number} params.completion_tokens - The number of completion tokens used.
|
|
* @param {string} params.model - The model used by the assistant run.
|
|
* @param {string} params.user - The user's ID.
|
|
* @param {string} params.conversationId - LibreChat conversation ID.
|
|
* @return {Promise<TMessage[]>} A promise that resolves to the updated messages
|
|
*/
|
|
const recordUsage = async ({ prompt_tokens, completion_tokens, model, user, conversationId }) => {
|
|
await spendTokens(
|
|
{
|
|
user,
|
|
model,
|
|
context: 'message',
|
|
conversationId,
|
|
},
|
|
{ promptTokens: prompt_tokens, completionTokens: completion_tokens },
|
|
);
|
|
};
|
|
|
|
module.exports = {
|
|
initThread,
|
|
recordUsage,
|
|
saveUserMessage,
|
|
checkMessageGaps,
|
|
addThreadMetadata,
|
|
mapMessagesToSteps,
|
|
saveAssistantMessage,
|
|
};
|