| const crypto = require('crypto'); |
| const fetch = require('node-fetch'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { |
| countTokens, |
| getBalanceConfig, |
| extractFileContext, |
| encodeAndFormatAudios, |
| encodeAndFormatVideos, |
| encodeAndFormatDocuments, |
| } = require('@librechat/api'); |
| const { |
| Constants, |
| ErrorTypes, |
| FileSources, |
| ContentTypes, |
| excludedKeys, |
| EModelEndpoint, |
| isParamEndpoint, |
| isAgentsEndpoint, |
| supportsBalanceCheck, |
| } = require('librechat-data-provider'); |
| const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models'); |
| const { getStrategyFunctions } = require('~/server/services/Files/strategies'); |
| const { checkBalance } = require('~/models/balanceMethods'); |
| const { truncateToolCallOutputs } = require('./prompts'); |
| const { getFiles } = require('~/models/File'); |
| const TextStream = require('./TextStream'); |
|
|
| class BaseClient { |
| constructor(apiKey, options = {}) { |
| this.apiKey = apiKey; |
| this.sender = options.sender ?? 'AI'; |
| this.contextStrategy = null; |
| this.currentDateString = new Date().toLocaleDateString('en-us', { |
| year: 'numeric', |
| month: 'long', |
| day: 'numeric', |
| }); |
| |
| this.skipSaveConvo = false; |
| |
| this.skipSaveUserMessage = false; |
| |
| this.user; |
| |
| this.conversationId; |
| |
| this.responseMessageId; |
| |
| this.parentMessageId; |
| |
| this.attachments; |
| |
| |
| this.inputTokensKey = 'prompt_tokens'; |
| |
| |
| this.outputTokensKey = 'completion_tokens'; |
| |
| this.savedMessageIds = new Set(); |
| |
| |
| |
| this.continued; |
| |
| |
| |
| this.fetchedConvo; |
| |
| this.currentMessages = []; |
| |
| this.visionMode; |
| } |
|
|
| setOptions() { |
| throw new Error("Method 'setOptions' must be implemented."); |
| } |
|
|
| async getCompletion() { |
| throw new Error("Method 'getCompletion' must be implemented."); |
| } |
|
|
| |
| async sendCompletion() { |
| throw new Error("Method 'sendCompletion' must be implemented."); |
| } |
|
|
| getSaveOptions() { |
| throw new Error('Subclasses must implement getSaveOptions'); |
| } |
|
|
| async buildMessages() { |
| throw new Error('Subclasses must implement buildMessages'); |
| } |
|
|
| async summarizeMessages() { |
| throw new Error('Subclasses attempted to call summarizeMessages without implementing it'); |
| } |
|
|
| |
| |
| |
| getResponseModel() { |
| if (isAgentsEndpoint(this.options.endpoint) && this.options.agent && this.options.agent.id) { |
| return this.options.agent.id; |
| } |
|
|
| return this.modelOptions?.model ?? this.model; |
| } |
|
|
| |
| |
| |
| |
| |
| getTokenCountForResponse(responseMessage) { |
| logger.debug('[BaseClient] `recordTokenUsage` not implemented.', responseMessage); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async recordTokenUsage({ model, balance, promptTokens, completionTokens }) { |
| logger.debug('[BaseClient] `recordTokenUsage` not implemented.', { |
| model, |
| balance, |
| promptTokens, |
| completionTokens, |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async fetch(_url, init) { |
| let url = _url; |
| if (this.options.directEndpoint) { |
| url = this.options.reverseProxyUrl; |
| } |
| logger.debug(`Making request to ${url}`); |
| if (typeof Bun !== 'undefined') { |
| return await fetch(url, init); |
| } |
| return await fetch(url, init); |
| } |
|
|
| getBuildMessagesOptions() { |
| throw new Error('Subclasses must implement getBuildMessagesOptions'); |
| } |
|
|
| async generateTextStream(text, onProgress, options = {}) { |
| const stream = new TextStream(text, options); |
| await stream.processTextStream(onProgress); |
| } |
|
|
| |
| |
| |
| processOverideIds() { |
| |
| let { overrideConvoId, overrideUserMessageId } = this.options?.req?.body ?? {}; |
| if (overrideConvoId) { |
| const [conversationId, index] = overrideConvoId.split(Constants.COMMON_DIVIDER); |
| overrideConvoId = conversationId; |
| if (index !== '0') { |
| this.skipSaveConvo = true; |
| } |
| } |
| if (overrideUserMessageId) { |
| const [userMessageId, index] = overrideUserMessageId.split(Constants.COMMON_DIVIDER); |
| overrideUserMessageId = userMessageId; |
| if (index !== '0') { |
| this.skipSaveUserMessage = true; |
| } |
| } |
|
|
| return [overrideConvoId, overrideUserMessageId]; |
| } |
|
|
| async setMessageOptions(opts = {}) { |
| if (opts && opts.replaceOptions) { |
| this.setOptions(opts); |
| } |
|
|
| const [overrideConvoId, overrideUserMessageId] = this.processOverideIds(); |
| const { isEdited, isContinued } = opts; |
| const user = opts.user ?? null; |
| this.user = user; |
| const saveOptions = this.getSaveOptions(); |
| this.abortController = opts.abortController ?? new AbortController(); |
| const requestConvoId = overrideConvoId ?? opts.conversationId; |
| const conversationId = requestConvoId ?? crypto.randomUUID(); |
| const parentMessageId = opts.parentMessageId ?? Constants.NO_PARENT; |
| const userMessageId = |
| overrideUserMessageId ?? opts.overrideParentMessageId ?? crypto.randomUUID(); |
| let responseMessageId = opts.responseMessageId ?? crypto.randomUUID(); |
| let head = isEdited ? responseMessageId : parentMessageId; |
| this.currentMessages = (await this.loadHistory(conversationId, head)) ?? []; |
| this.conversationId = conversationId; |
|
|
| if (isEdited && !isContinued) { |
| responseMessageId = crypto.randomUUID(); |
| head = responseMessageId; |
| this.currentMessages[this.currentMessages.length - 1].messageId = head; |
| } |
|
|
| if (opts.isRegenerate && responseMessageId.endsWith('_')) { |
| responseMessageId = crypto.randomUUID(); |
| } |
|
|
| this.responseMessageId = responseMessageId; |
|
|
| return { |
| ...opts, |
| user, |
| head, |
| saveOptions, |
| userMessageId, |
| requestConvoId, |
| conversationId, |
| parentMessageId, |
| responseMessageId, |
| }; |
| } |
|
|
| createUserMessage({ messageId, parentMessageId, conversationId, text }) { |
| return { |
| messageId, |
| parentMessageId, |
| conversationId, |
| sender: 'User', |
| text, |
| isCreatedByUser: true, |
| }; |
| } |
|
|
| async handleStartMethods(message, opts) { |
| const { |
| user, |
| head, |
| saveOptions, |
| userMessageId, |
| requestConvoId, |
| conversationId, |
| parentMessageId, |
| responseMessageId, |
| } = await this.setMessageOptions(opts); |
|
|
| const userMessage = opts.isEdited |
| ? this.currentMessages[this.currentMessages.length - 2] |
| : this.createUserMessage({ |
| messageId: userMessageId, |
| parentMessageId, |
| conversationId, |
| text: message, |
| }); |
|
|
| if (typeof opts?.getReqData === 'function') { |
| opts.getReqData({ |
| userMessage, |
| conversationId, |
| responseMessageId, |
| sender: this.sender, |
| }); |
| } |
|
|
| if (typeof opts?.onStart === 'function') { |
| const isNewConvo = !requestConvoId && parentMessageId === Constants.NO_PARENT; |
| opts.onStart(userMessage, responseMessageId, isNewConvo); |
| } |
|
|
| return { |
| ...opts, |
| user, |
| head, |
| conversationId, |
| responseMessageId, |
| saveOptions, |
| userMessage, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| addInstructions(messages, instructions, beforeLast = false) { |
| if (!instructions || Object.keys(instructions).length === 0) { |
| return messages; |
| } |
|
|
| if (!beforeLast) { |
| return [instructions, ...messages]; |
| } |
|
|
| |
| const payload = []; |
| if (messages.length > 1) { |
| payload.push(...messages.slice(0, -1)); |
| } |
|
|
| payload.push(instructions); |
|
|
| if (messages.length > 0) { |
| payload.push(messages[messages.length - 1]); |
| } |
|
|
| return payload; |
| } |
|
|
| async handleTokenCountMap(tokenCountMap) { |
| if (this.clientName === EModelEndpoint.agents) { |
| return; |
| } |
| if (this.currentMessages.length === 0) { |
| return; |
| } |
|
|
| for (let i = 0; i < this.currentMessages.length; i++) { |
| |
| if (i === this.currentMessages.length - 1) { |
| break; |
| } |
|
|
| const message = this.currentMessages[i]; |
| const { messageId } = message; |
| const update = {}; |
|
|
| if (messageId === tokenCountMap.summaryMessage?.messageId) { |
| logger.debug(`[BaseClient] Adding summary props to ${messageId}.`); |
|
|
| update.summary = tokenCountMap.summaryMessage.content; |
| update.summaryTokenCount = tokenCountMap.summaryMessage.tokenCount; |
| } |
|
|
| if (message.tokenCount && !update.summaryTokenCount) { |
| logger.debug(`[BaseClient] Skipping ${messageId}: already had a token count.`); |
| continue; |
| } |
|
|
| const tokenCount = tokenCountMap[messageId]; |
| if (tokenCount) { |
| message.tokenCount = tokenCount; |
| update.tokenCount = tokenCount; |
| await this.updateMessageInDatabase({ messageId, ...update }); |
| } |
| } |
| } |
|
|
| concatenateMessages(messages) { |
| return messages.reduce((acc, message) => { |
| const nameOrRole = message.name ?? message.role; |
| return acc + `${nameOrRole}:\n${message.content}\n\n`; |
| }, ''); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async getMessagesWithinTokenLimit({ messages: _messages, maxContextTokens, instructions }) { |
| |
| |
| let currentTokenCount = 3; |
| const instructionsTokenCount = instructions?.tokenCount ?? 0; |
| let remainingContextTokens = |
| (maxContextTokens ?? this.maxContextTokens) - instructionsTokenCount; |
| const messages = [..._messages]; |
|
|
| const context = []; |
|
|
| if (currentTokenCount < remainingContextTokens) { |
| while (messages.length > 0 && currentTokenCount < remainingContextTokens) { |
| if (messages.length === 1 && instructions) { |
| break; |
| } |
| const poppedMessage = messages.pop(); |
| const { tokenCount } = poppedMessage; |
|
|
| if (poppedMessage && currentTokenCount + tokenCount <= remainingContextTokens) { |
| context.push(poppedMessage); |
| currentTokenCount += tokenCount; |
| } else { |
| messages.push(poppedMessage); |
| break; |
| } |
| } |
| } |
|
|
| if (instructions) { |
| context.push(_messages[0]); |
| messages.shift(); |
| } |
|
|
| const prunedMemory = messages; |
| remainingContextTokens -= currentTokenCount; |
|
|
| return { |
| context: context.reverse(), |
| remainingContextTokens, |
| messagesToRefine: prunedMemory, |
| }; |
| } |
|
|
| async handleContextStrategy({ |
| instructions, |
| orderedMessages, |
| formattedMessages, |
| buildTokenMap = true, |
| }) { |
| let _instructions; |
| let tokenCount; |
|
|
| if (instructions) { |
| ({ tokenCount, ..._instructions } = instructions); |
| } |
|
|
| _instructions && logger.debug('[BaseClient] instructions tokenCount: ' + tokenCount); |
| if (tokenCount && tokenCount > this.maxContextTokens) { |
| const info = `${tokenCount} / ${this.maxContextTokens}`; |
| const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; |
| logger.warn(`Instructions token count exceeds max token count (${info}).`); |
| throw new Error(errorMessage); |
| } |
|
|
| if (this.clientName === EModelEndpoint.agents) { |
| const { dbMessages, editedIndices } = truncateToolCallOutputs( |
| orderedMessages, |
| this.maxContextTokens, |
| this.getTokenCountForMessage.bind(this), |
| ); |
|
|
| if (editedIndices.length > 0) { |
| logger.debug('[BaseClient] Truncated tool call outputs:', editedIndices); |
| for (const index of editedIndices) { |
| formattedMessages[index].content = dbMessages[index].content; |
| } |
| orderedMessages = dbMessages; |
| } |
| } |
|
|
| let orderedWithInstructions = this.addInstructions(orderedMessages, instructions); |
|
|
| let { context, remainingContextTokens, messagesToRefine } = |
| await this.getMessagesWithinTokenLimit({ |
| messages: orderedWithInstructions, |
| instructions, |
| }); |
|
|
| logger.debug('[BaseClient] Context Count (1/2)', { |
| remainingContextTokens, |
| maxContextTokens: this.maxContextTokens, |
| }); |
|
|
| let summaryMessage; |
| let summaryTokenCount; |
| let { shouldSummarize } = this; |
|
|
| |
| let payload; |
| let { length } = formattedMessages; |
| length += instructions != null ? 1 : 0; |
| const diff = length - context.length; |
| const firstMessage = orderedWithInstructions[0]; |
| const usePrevSummary = |
| shouldSummarize && |
| diff === 1 && |
| firstMessage?.summary && |
| this.previous_summary.messageId === firstMessage.messageId; |
|
|
| if (diff > 0) { |
| payload = formattedMessages.slice(diff); |
| logger.debug( |
| `[BaseClient] Difference between original payload (${length}) and context (${context.length}): ${diff}`, |
| ); |
| } |
|
|
| payload = this.addInstructions(payload ?? formattedMessages, _instructions); |
|
|
| const latestMessage = orderedWithInstructions[orderedWithInstructions.length - 1]; |
| if (payload.length === 0 && !shouldSummarize && latestMessage) { |
| const info = `${latestMessage.tokenCount} / ${this.maxContextTokens}`; |
| const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; |
| logger.warn(`Prompt token count exceeds max token count (${info}).`); |
| throw new Error(errorMessage); |
| } else if ( |
| _instructions && |
| payload.length === 1 && |
| payload[0].content === _instructions.content |
| ) { |
| const info = `${tokenCount + 3} / ${this.maxContextTokens}`; |
| const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; |
| logger.warn( |
| `Including instructions, the prompt token count exceeds remaining max token count (${info}).`, |
| ); |
| throw new Error(errorMessage); |
| } |
|
|
| if (usePrevSummary) { |
| summaryMessage = { role: 'system', content: firstMessage.summary }; |
| summaryTokenCount = firstMessage.summaryTokenCount; |
| payload.unshift(summaryMessage); |
| remainingContextTokens -= summaryTokenCount; |
| } else if (shouldSummarize && messagesToRefine.length > 0) { |
| ({ summaryMessage, summaryTokenCount } = await this.summarizeMessages({ |
| messagesToRefine, |
| remainingContextTokens, |
| })); |
| summaryMessage && payload.unshift(summaryMessage); |
| remainingContextTokens -= summaryTokenCount; |
| } |
|
|
| |
| shouldSummarize = summaryMessage != null && shouldSummarize === true; |
|
|
| logger.debug('[BaseClient] Context Count (2/2)', { |
| remainingContextTokens, |
| maxContextTokens: this.maxContextTokens, |
| }); |
|
|
| |
| let tokenCountMap; |
| if (buildTokenMap) { |
| const currentPayload = shouldSummarize ? orderedWithInstructions : context; |
| tokenCountMap = currentPayload.reduce((map, message, index) => { |
| const { messageId } = message; |
| if (!messageId) { |
| return map; |
| } |
|
|
| if (shouldSummarize && index === messagesToRefine.length - 1 && !usePrevSummary) { |
| map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount }; |
| } |
|
|
| map[messageId] = currentPayload[index].tokenCount; |
| return map; |
| }, {}); |
| } |
|
|
| const promptTokens = this.maxContextTokens - remainingContextTokens; |
|
|
| logger.debug('[BaseClient] tokenCountMap:', tokenCountMap); |
| logger.debug('[BaseClient]', { |
| promptTokens, |
| remainingContextTokens, |
| payloadSize: payload.length, |
| maxContextTokens: this.maxContextTokens, |
| }); |
|
|
| return { payload, tokenCountMap, promptTokens, messages: orderedWithInstructions }; |
| } |
|
|
| async sendMessage(message, opts = {}) { |
| const appConfig = this.options.req?.config; |
| |
| let userMessagePromise; |
| const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } = |
| await this.handleStartMethods(message, opts); |
|
|
| if (opts.progressCallback) { |
| opts.onProgress = opts.progressCallback.call(null, { |
| ...(opts.progressOptions ?? {}), |
| parentMessageId: userMessage.messageId, |
| messageId: responseMessageId, |
| }); |
| } |
|
|
| const { editedContent } = opts; |
|
|
| |
| |
| |
| if (isEdited) { |
| let latestMessage = this.currentMessages[this.currentMessages.length - 1]; |
| if (!latestMessage) { |
| latestMessage = { |
| messageId: responseMessageId, |
| conversationId, |
| parentMessageId: userMessage.messageId, |
| isCreatedByUser: false, |
| model: this.modelOptions?.model ?? this.model, |
| sender: this.sender, |
| }; |
| this.currentMessages.push(userMessage, latestMessage); |
| } else if (editedContent != null) { |
| |
| if (editedContent && latestMessage.content && Array.isArray(latestMessage.content)) { |
| const { index, text, type } = editedContent; |
| if (index >= 0 && index < latestMessage.content.length) { |
| const contentPart = latestMessage.content[index]; |
| if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) { |
| contentPart[ContentTypes.THINK] = text; |
| } else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) { |
| contentPart[ContentTypes.TEXT] = text; |
| } |
| } |
| } |
| } |
| this.continued = true; |
| } else { |
| this.currentMessages.push(userMessage); |
| } |
|
|
| |
| |
| |
| |
| const parentMessageId = isEdited ? head : userMessage.messageId; |
| this.parentMessageId = parentMessageId; |
| let { |
| prompt: payload, |
| tokenCountMap, |
| promptTokens, |
| } = await this.buildMessages( |
| this.currentMessages, |
| parentMessageId, |
| this.getBuildMessagesOptions(opts), |
| opts, |
| ); |
|
|
| if (tokenCountMap) { |
| logger.debug('[BaseClient] tokenCountMap', tokenCountMap); |
| if (tokenCountMap[userMessage.messageId]) { |
| userMessage.tokenCount = tokenCountMap[userMessage.messageId]; |
| logger.debug('[BaseClient] userMessage', userMessage); |
| } |
|
|
| this.handleTokenCountMap(tokenCountMap); |
| } |
|
|
| if (!isEdited && !this.skipSaveUserMessage) { |
| userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user); |
| this.savedMessageIds.add(userMessage.messageId); |
| if (typeof opts?.getReqData === 'function') { |
| opts.getReqData({ |
| userMessagePromise, |
| }); |
| } |
| } |
|
|
| const balanceConfig = getBalanceConfig(appConfig); |
| if ( |
| balanceConfig?.enabled && |
| supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint] |
| ) { |
| await checkBalance({ |
| req: this.options.req, |
| res: this.options.res, |
| txData: { |
| user: this.user, |
| tokenType: 'prompt', |
| amount: promptTokens, |
| endpoint: this.options.endpoint, |
| model: this.modelOptions?.model ?? this.model, |
| endpointTokenConfig: this.options.endpointTokenConfig, |
| }, |
| }); |
| } |
|
|
| const { completion, metadata } = await this.sendCompletion(payload, opts); |
| if (this.abortController) { |
| this.abortController.requestCompleted = true; |
| } |
|
|
| |
| const responseMessage = { |
| messageId: responseMessageId, |
| conversationId, |
| parentMessageId: userMessage.messageId, |
| isCreatedByUser: false, |
| isEdited, |
| model: this.getResponseModel(), |
| sender: this.sender, |
| promptTokens, |
| iconURL: this.options.iconURL, |
| endpoint: this.options.endpoint, |
| ...(this.metadata ?? {}), |
| metadata, |
| }; |
|
|
| if (typeof completion === 'string') { |
| responseMessage.text = completion; |
| } else if ( |
| Array.isArray(completion) && |
| (this.clientName === EModelEndpoint.agents || |
| isParamEndpoint(this.options.endpoint, this.options.endpointType)) |
| ) { |
| responseMessage.text = ''; |
|
|
| if (!opts.editedContent || this.currentMessages.length === 0) { |
| responseMessage.content = completion; |
| } else { |
| const latestMessage = this.currentMessages[this.currentMessages.length - 1]; |
| if (!latestMessage?.content) { |
| responseMessage.content = completion; |
| } else { |
| const existingContent = [...latestMessage.content]; |
| const { type: editedType } = opts.editedContent; |
| responseMessage.content = this.mergeEditedContent( |
| existingContent, |
| completion, |
| editedType, |
| ); |
| } |
| } |
| } else if (Array.isArray(completion)) { |
| responseMessage.text = completion.join(''); |
| } |
|
|
| if ( |
| tokenCountMap && |
| this.recordTokenUsage && |
| this.getTokenCountForResponse && |
| this.getTokenCount |
| ) { |
| let completionTokens; |
|
|
| |
| |
| |
| |
| |
| const usage = this.getStreamUsage != null ? this.getStreamUsage() : null; |
|
|
| if (usage != null && Number(usage[this.outputTokensKey]) > 0) { |
| responseMessage.tokenCount = usage[this.outputTokensKey]; |
| completionTokens = responseMessage.tokenCount; |
| await this.updateUserMessageTokenCount({ |
| usage, |
| tokenCountMap, |
| userMessage, |
| userMessagePromise, |
| opts, |
| }); |
| } else { |
| responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage); |
| completionTokens = responseMessage.tokenCount; |
| await this.recordTokenUsage({ |
| usage, |
| promptTokens, |
| completionTokens, |
| balance: balanceConfig, |
| model: responseMessage.model, |
| }); |
| } |
| } |
|
|
| if (userMessagePromise) { |
| await userMessagePromise; |
| } |
|
|
| if (this.artifactPromises) { |
| responseMessage.attachments = (await Promise.all(this.artifactPromises)).filter((a) => a); |
| } |
|
|
| if (this.options.attachments) { |
| try { |
| saveOptions.files = this.options.attachments.map((attachments) => attachments.file_id); |
| } catch (error) { |
| logger.error('[BaseClient] Error mapping attachments for conversation', error); |
| } |
| } |
|
|
| responseMessage.databasePromise = this.saveMessageToDatabase( |
| responseMessage, |
| saveOptions, |
| user, |
| ); |
| this.savedMessageIds.add(responseMessage.messageId); |
| delete responseMessage.tokenCount; |
| return responseMessage; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async updateUserMessageTokenCount({ |
| usage, |
| tokenCountMap, |
| userMessage, |
| userMessagePromise, |
| opts, |
| }) { |
| |
| const shouldUpdateCount = |
| this.calculateCurrentTokenCount != null && |
| Number(usage[this.inputTokensKey]) > 0 && |
| (this.options.resendFiles || |
| (!this.options.resendFiles && !this.options.attachments?.length)) && |
| !this.options.promptPrefix; |
|
|
| if (!shouldUpdateCount) { |
| return; |
| } |
|
|
| const userMessageTokenCount = this.calculateCurrentTokenCount({ |
| currentMessageId: userMessage.messageId, |
| tokenCountMap, |
| usage, |
| }); |
|
|
| if (userMessageTokenCount === userMessage.tokenCount) { |
| return; |
| } |
|
|
| userMessage.tokenCount = userMessageTokenCount; |
| |
| |
| |
| |
| if (typeof opts?.getReqData === 'function') { |
| opts.getReqData({ |
| userMessage, |
| }); |
| } |
| |
| |
| |
| |
| |
| await userMessagePromise; |
| await this.updateMessageInDatabase({ |
| messageId: userMessage.messageId, |
| tokenCount: userMessageTokenCount, |
| }); |
| } |
|
|
| async loadHistory(conversationId, parentMessageId = null) { |
| logger.debug('[BaseClient] Loading history:', { conversationId, parentMessageId }); |
|
|
| const messages = (await getMessages({ conversationId })) ?? []; |
|
|
| if (messages.length === 0) { |
| return []; |
| } |
|
|
| let mapMethod = null; |
| if (this.getMessageMapMethod) { |
| mapMethod = this.getMessageMapMethod(); |
| } |
|
|
| let _messages = this.constructor.getMessagesForConversation({ |
| messages, |
| parentMessageId, |
| mapMethod, |
| }); |
|
|
| _messages = await this.addPreviousAttachments(_messages); |
|
|
| if (!this.shouldSummarize) { |
| return _messages; |
| } |
|
|
| |
| for (let i = _messages.length - 1; i >= 0; i--) { |
| if (_messages[i]?.summary) { |
| this.previous_summary = _messages[i]; |
| break; |
| } |
| } |
|
|
| if (this.previous_summary) { |
| const { messageId, summary, tokenCount, summaryTokenCount } = this.previous_summary; |
| logger.debug('[BaseClient] Previous summary:', { |
| messageId, |
| summary, |
| tokenCount, |
| summaryTokenCount, |
| }); |
| } |
|
|
| return _messages; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async saveMessageToDatabase(message, endpointOptions, user = null) { |
| if (this.user && user !== this.user) { |
| throw new Error('User mismatch.'); |
| } |
|
|
| const savedMessage = await saveMessage( |
| this.options?.req, |
| { |
| ...message, |
| endpoint: this.options.endpoint, |
| unfinished: false, |
| user, |
| }, |
| { context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveMessage' }, |
| ); |
|
|
| if (this.skipSaveConvo) { |
| return { message: savedMessage }; |
| } |
|
|
| const fieldsToKeep = { |
| conversationId: message.conversationId, |
| endpoint: this.options.endpoint, |
| endpointType: this.options.endpointType, |
| ...endpointOptions, |
| }; |
|
|
| const existingConvo = |
| this.fetchedConvo === true |
| ? null |
| : await getConvo(this.options?.req?.user?.id, message.conversationId); |
|
|
| const unsetFields = {}; |
| const exceptions = new Set(['spec', 'iconURL']); |
| if (existingConvo != null) { |
| this.fetchedConvo = true; |
| for (const key in existingConvo) { |
| if (!key) { |
| continue; |
| } |
| if (excludedKeys.has(key) && !exceptions.has(key)) { |
| continue; |
| } |
|
|
| if (endpointOptions?.[key] === undefined) { |
| unsetFields[key] = 1; |
| } |
| } |
| } |
|
|
| const conversation = await saveConvo(this.options?.req, fieldsToKeep, { |
| context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo', |
| unsetFields, |
| }); |
|
|
| return { message: savedMessage, conversation }; |
| } |
|
|
| |
| |
| |
| |
| async updateMessageInDatabase(message) { |
| await updateMessage(this.options.req, message); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| static getMessagesForConversation({ |
| messages, |
| parentMessageId, |
| mapMethod = null, |
| summary = false, |
| }) { |
| if (!messages || messages.length === 0) { |
| return []; |
| } |
|
|
| const orderedMessages = []; |
| let currentMessageId = parentMessageId; |
| const visitedMessageIds = new Set(); |
|
|
| while (currentMessageId) { |
| if (visitedMessageIds.has(currentMessageId)) { |
| break; |
| } |
| const message = messages.find((msg) => { |
| const messageId = msg.messageId ?? msg.id; |
| return messageId === currentMessageId; |
| }); |
|
|
| visitedMessageIds.add(currentMessageId); |
|
|
| if (!message) { |
| break; |
| } |
|
|
| if (summary && message.summary) { |
| message.role = 'system'; |
| message.text = message.summary; |
| } |
|
|
| if (summary && message.summaryTokenCount) { |
| message.tokenCount = message.summaryTokenCount; |
| } |
|
|
| orderedMessages.push(message); |
|
|
| if (summary && message.summary) { |
| break; |
| } |
|
|
| currentMessageId = |
| message.parentMessageId === Constants.NO_PARENT ? null : message.parentMessageId; |
| } |
|
|
| orderedMessages.reverse(); |
|
|
| if (mapMethod) { |
| return orderedMessages.map(mapMethod); |
| } |
|
|
| return orderedMessages; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| getTokenCountForMessage(message) { |
| |
| let tokensPerMessage = 3; |
| let tokensPerName = 1; |
| const model = this.modelOptions?.model ?? this.model; |
|
|
| if (model === 'gpt-3.5-turbo-0301') { |
| tokensPerMessage = 4; |
| tokensPerName = -1; |
| } |
|
|
| const processValue = (value) => { |
| if (Array.isArray(value)) { |
| for (let item of value) { |
| if ( |
| !item || |
| !item.type || |
| item.type === ContentTypes.THINK || |
| item.type === ContentTypes.ERROR || |
| item.type === ContentTypes.IMAGE_URL |
| ) { |
| continue; |
| } |
|
|
| if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) { |
| const toolName = item.tool_call?.name || ''; |
| if (toolName != null && toolName && typeof toolName === 'string') { |
| numTokens += this.getTokenCount(toolName); |
| } |
|
|
| const args = item.tool_call?.args || ''; |
| if (args != null && args && typeof args === 'string') { |
| numTokens += this.getTokenCount(args); |
| } |
|
|
| const output = item.tool_call?.output || ''; |
| if (output != null && output && typeof output === 'string') { |
| numTokens += this.getTokenCount(output); |
| } |
| continue; |
| } |
|
|
| const nestedValue = item[item.type]; |
|
|
| if (!nestedValue) { |
| continue; |
| } |
|
|
| processValue(nestedValue); |
| } |
| } else if (typeof value === 'string') { |
| numTokens += this.getTokenCount(value); |
| } else if (typeof value === 'number') { |
| numTokens += this.getTokenCount(value.toString()); |
| } else if (typeof value === 'boolean') { |
| numTokens += this.getTokenCount(value.toString()); |
| } |
| }; |
|
|
| let numTokens = tokensPerMessage; |
| for (let [key, value] of Object.entries(message)) { |
| processValue(value); |
|
|
| if (key === 'name') { |
| numTokens += tokensPerName; |
| } |
| } |
| return numTokens; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| mergeEditedContent(existingContent, newCompletion, editedType) { |
| if (!newCompletion.length) { |
| return existingContent.concat(newCompletion); |
| } |
|
|
| if (editedType !== ContentTypes.TEXT && editedType !== ContentTypes.THINK) { |
| return existingContent.concat(newCompletion); |
| } |
|
|
| const lastIndex = existingContent.length - 1; |
| const lastExisting = existingContent[lastIndex]; |
| const firstNew = newCompletion[0]; |
|
|
| if (lastExisting?.type !== firstNew?.type || firstNew?.type !== editedType) { |
| return existingContent.concat(newCompletion); |
| } |
|
|
| const mergedContent = [...existingContent]; |
| if (editedType === ContentTypes.TEXT) { |
| mergedContent[lastIndex] = { |
| ...mergedContent[lastIndex], |
| [ContentTypes.TEXT]: |
| (mergedContent[lastIndex][ContentTypes.TEXT] || '') + (firstNew[ContentTypes.TEXT] || ''), |
| }; |
| } else { |
| mergedContent[lastIndex] = { |
| ...mergedContent[lastIndex], |
| [ContentTypes.THINK]: |
| (mergedContent[lastIndex][ContentTypes.THINK] || '') + |
| (firstNew[ContentTypes.THINK] || ''), |
| }; |
| } |
|
|
| |
| return mergedContent.concat(newCompletion.slice(1)); |
| } |
|
|
| async sendPayload(payload, opts = {}) { |
| if (opts && typeof opts === 'object') { |
| this.setOptions(opts); |
| } |
|
|
| return await this.sendCompletion(payload, opts); |
| } |
|
|
| async addDocuments(message, attachments) { |
| const documentResult = await encodeAndFormatDocuments( |
| this.options.req, |
| attachments, |
| { |
| provider: this.options.agent?.provider ?? this.options.endpoint, |
| endpoint: this.options.agent?.endpoint ?? this.options.endpoint, |
| useResponsesApi: this.options.agent?.model_parameters?.useResponsesApi, |
| }, |
| getStrategyFunctions, |
| ); |
| message.documents = |
| documentResult.documents && documentResult.documents.length |
| ? documentResult.documents |
| : undefined; |
| return documentResult.files; |
| } |
|
|
| async addVideos(message, attachments) { |
| const videoResult = await encodeAndFormatVideos( |
| this.options.req, |
| attachments, |
| { |
| provider: this.options.agent?.provider ?? this.options.endpoint, |
| endpoint: this.options.agent?.endpoint ?? this.options.endpoint, |
| }, |
| getStrategyFunctions, |
| ); |
| message.videos = |
| videoResult.videos && videoResult.videos.length ? videoResult.videos : undefined; |
| return videoResult.files; |
| } |
|
|
| async addAudios(message, attachments) { |
| const audioResult = await encodeAndFormatAudios( |
| this.options.req, |
| attachments, |
| { |
| provider: this.options.agent?.provider ?? this.options.endpoint, |
| endpoint: this.options.agent?.endpoint ?? this.options.endpoint, |
| }, |
| getStrategyFunctions, |
| ); |
| message.audios = |
| audioResult.audios && audioResult.audios.length ? audioResult.audios : undefined; |
| return audioResult.files; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async addFileContextToMessage(message, attachments) { |
| const fileContext = await extractFileContext({ |
| attachments, |
| req: this.options?.req, |
| tokenCountFn: (text) => countTokens(text), |
| }); |
|
|
| if (fileContext) { |
| message.fileContext = fileContext; |
| } |
| } |
|
|
| async processAttachments(message, attachments) { |
| const categorizedAttachments = { |
| images: [], |
| videos: [], |
| audios: [], |
| documents: [], |
| }; |
|
|
| const allFiles = []; |
|
|
| for (const file of attachments) { |
| |
| const source = file.source ?? FileSources.local; |
| if (source === FileSources.text) { |
| allFiles.push(file); |
| continue; |
| } |
| if (file.embedded === true || file.metadata?.fileIdentifier != null) { |
| allFiles.push(file); |
| continue; |
| } |
|
|
| if (file.type.startsWith('image/')) { |
| categorizedAttachments.images.push(file); |
| } else if (file.type === 'application/pdf') { |
| categorizedAttachments.documents.push(file); |
| allFiles.push(file); |
| } else if (file.type.startsWith('video/')) { |
| categorizedAttachments.videos.push(file); |
| allFiles.push(file); |
| } else if (file.type.startsWith('audio/')) { |
| categorizedAttachments.audios.push(file); |
| allFiles.push(file); |
| } |
| } |
|
|
| const [imageFiles] = await Promise.all([ |
| categorizedAttachments.images.length > 0 |
| ? this.addImageURLs(message, categorizedAttachments.images) |
| : Promise.resolve([]), |
| categorizedAttachments.documents.length > 0 |
| ? this.addDocuments(message, categorizedAttachments.documents) |
| : Promise.resolve([]), |
| categorizedAttachments.videos.length > 0 |
| ? this.addVideos(message, categorizedAttachments.videos) |
| : Promise.resolve([]), |
| categorizedAttachments.audios.length > 0 |
| ? this.addAudios(message, categorizedAttachments.audios) |
| : Promise.resolve([]), |
| ]); |
|
|
| allFiles.push(...imageFiles); |
|
|
| const seenFileIds = new Set(); |
| const uniqueFiles = []; |
|
|
| for (const file of allFiles) { |
| if (file.file_id && !seenFileIds.has(file.file_id)) { |
| seenFileIds.add(file.file_id); |
| uniqueFiles.push(file); |
| } else if (!file.file_id) { |
| uniqueFiles.push(file); |
| } |
| } |
|
|
| return uniqueFiles; |
| } |
|
|
| |
| |
| |
| |
| async addPreviousAttachments(_messages) { |
| if (!this.options.resendFiles) { |
| return _messages; |
| } |
|
|
| const seen = new Set(); |
| const attachmentsProcessed = |
| this.options.attachments && !(this.options.attachments instanceof Promise); |
| if (attachmentsProcessed) { |
| for (const attachment of this.options.attachments) { |
| seen.add(attachment.file_id); |
| } |
| } |
|
|
| |
| |
| |
| |
| const processMessage = async (message) => { |
| if (!this.message_file_map) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |