| | 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) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |