| | require('events').EventEmitter.defaultMaxListeners = 100; |
| | const { logger } = require('@librechat/data-schemas'); |
| | const { DynamicStructuredTool } = require('@langchain/core/tools'); |
| | const { getBufferString, HumanMessage } = require('@langchain/core/messages'); |
| | const { |
| | createRun, |
| | Tokenizer, |
| | checkAccess, |
| | logAxiosError, |
| | sanitizeTitle, |
| | resolveHeaders, |
| | createSafeUser, |
| | getBalanceConfig, |
| | memoryInstructions, |
| | getTransactionsConfig, |
| | createMemoryProcessor, |
| | filterMalformedContentParts, |
| | } = require('@librechat/api'); |
| | const { |
| | Callback, |
| | Providers, |
| | TitleMethod, |
| | formatMessage, |
| | labelContentByAgent, |
| | formatAgentMessages, |
| | getTokenCountForMessage, |
| | createMetadataAggregator, |
| | } = require('@librechat/agents'); |
| | const { |
| | Constants, |
| | Permissions, |
| | VisionModes, |
| | ContentTypes, |
| | EModelEndpoint, |
| | PermissionTypes, |
| | isAgentsEndpoint, |
| | AgentCapabilities, |
| | bedrockInputSchema, |
| | removeNullishValues, |
| | } = require('librechat-data-provider'); |
| | const { initializeAgent } = require('~/server/services/Endpoints/agents/agent'); |
| | const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); |
| | const { getFormattedMemories, deleteMemory, setMemory } = require('~/models'); |
| | const { encodeAndFormat } = require('~/server/services/Files/images/encode'); |
| | const { getProviderConfig } = require('~/server/services/Endpoints'); |
| | const { createContextHandlers } = require('~/app/clients/prompts'); |
| | const { checkCapability } = require('~/server/services/Config'); |
| | const BaseClient = require('~/app/clients/BaseClient'); |
| | const { getRoleByName } = require('~/models/Role'); |
| | const { loadAgent } = require('~/models/Agent'); |
| | const { getMCPManager } = require('~/config'); |
| |
|
| | const omitTitleOptions = new Set([ |
| | 'stream', |
| | 'thinking', |
| | 'streaming', |
| | 'clientOptions', |
| | 'thinkingConfig', |
| | 'thinkingBudget', |
| | 'includeThoughts', |
| | 'maxOutputTokens', |
| | 'additionalModelRequestFields', |
| | ]); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const payloadParser = ({ req, agent, endpoint }) => { |
| | if (isAgentsEndpoint(endpoint)) { |
| | return { model: undefined }; |
| | } else if (endpoint === EModelEndpoint.bedrock) { |
| | const parsedValues = bedrockInputSchema.parse(agent.model_parameters); |
| | if (parsedValues.thinking == null) { |
| | parsedValues.thinking = false; |
| | } |
| | return parsedValues; |
| | } |
| | return req.body.endpointOption.model_parameters; |
| | }; |
| |
|
| | function createTokenCounter(encoding) { |
| | return function (message) { |
| | const countTokens = (text) => Tokenizer.getTokenCount(text, encoding); |
| | return getTokenCountForMessage(message, countTokens); |
| | }; |
| | } |
| |
|
| | function logToolError(graph, error, toolId) { |
| | logAxiosError({ |
| | error, |
| | message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`, |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function applyAgentLabelsToHistory(orderedMessages, primaryAgent, agentConfigs) { |
| | const shouldLabelByAgent = (primaryAgent.edges?.length ?? 0) > 0 || (agentConfigs?.size ?? 0) > 0; |
| |
|
| | if (!shouldLabelByAgent) { |
| | return orderedMessages; |
| | } |
| |
|
| | const processedMessages = []; |
| |
|
| | for (let i = 0; i < orderedMessages.length; i++) { |
| | const message = orderedMessages[i]; |
| |
|
| | |
| | const agentNames = { [primaryAgent.id]: primaryAgent.name || 'Assistant' }; |
| |
|
| | if (agentConfigs) { |
| | for (const [agentId, agentConfig] of agentConfigs.entries()) { |
| | agentNames[agentId] = agentConfig.name || agentConfig.id; |
| | } |
| | } |
| |
|
| | if ( |
| | !message.isCreatedByUser && |
| | message.metadata?.agentIdMap && |
| | Array.isArray(message.content) |
| | ) { |
| | try { |
| | const labeledContent = labelContentByAgent( |
| | message.content, |
| | message.metadata.agentIdMap, |
| | agentNames, |
| | ); |
| |
|
| | processedMessages.push({ ...message, content: labeledContent }); |
| | } catch (error) { |
| | logger.error('[AgentClient] Error applying agent labels to message:', error); |
| | processedMessages.push(message); |
| | } |
| | } else { |
| | processedMessages.push(message); |
| | } |
| | } |
| |
|
| | return processedMessages; |
| | } |
| |
|
| | class AgentClient extends BaseClient { |
| | constructor(options = {}) { |
| | super(null, options); |
| | |
| | |
| | this.clientName = EModelEndpoint.agents; |
| |
|
| | |
| | this.contextStrategy = 'discard'; |
| |
|
| | |
| | this.isChatCompletion = true; |
| |
|
| | |
| | this.run; |
| |
|
| | const { |
| | agentConfigs, |
| | contentParts, |
| | collectedUsage, |
| | artifactPromises, |
| | maxContextTokens, |
| | ...clientOptions |
| | } = options; |
| |
|
| | this.agentConfigs = agentConfigs; |
| | this.maxContextTokens = maxContextTokens; |
| | |
| | this.contentParts = contentParts; |
| | |
| | this.collectedUsage = collectedUsage; |
| | |
| | this.artifactPromises = artifactPromises; |
| | |
| | this.options = Object.assign({ endpoint: options.endpoint }, clientOptions); |
| | |
| | this.model = this.options.agent.model_parameters.model; |
| | |
| | |
| | this.inputTokensKey = 'input_tokens'; |
| | |
| | |
| | this.outputTokensKey = 'output_tokens'; |
| | |
| | this.usage; |
| | |
| | this.indexTokenCountMap = {}; |
| | |
| | this.processMemory; |
| | |
| | this.agentIdMap = null; |
| | } |
| |
|
| | |
| | |
| | |
| | getContentParts() { |
| | return this.contentParts; |
| | } |
| |
|
| | setOptions(options) { |
| | logger.info('[api/server/controllers/agents/client.js] setOptions', options); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | checkVisionRequest() {} |
| |
|
| | getSaveOptions() { |
| | |
| | |
| | |
| | |
| | |
| | let runOptions = {}; |
| | try { |
| | runOptions = payloadParser(this.options); |
| | } catch (error) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #getSaveOptions] Error parsing options', |
| | error, |
| | ); |
| | } |
| |
|
| | return removeNullishValues( |
| | Object.assign( |
| | { |
| | endpoint: this.options.endpoint, |
| | agent_id: this.options.agent.id, |
| | modelLabel: this.options.modelLabel, |
| | maxContextTokens: this.options.maxContextTokens, |
| | resendFiles: this.options.resendFiles, |
| | imageDetail: this.options.imageDetail, |
| | spec: this.options.spec, |
| | iconURL: this.options.iconURL, |
| | }, |
| | |
| | runOptions, |
| | ), |
| | ); |
| | } |
| |
|
| | getBuildMessagesOptions() { |
| | return { |
| | instructions: this.options.agent.instructions, |
| | additional_instructions: this.options.agent.additional_instructions, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async addImageURLs(message, attachments) { |
| | const { files, image_urls } = await encodeAndFormat( |
| | this.options.req, |
| | attachments, |
| | { |
| | provider: this.options.agent.provider, |
| | endpoint: this.options.endpoint, |
| | }, |
| | VisionModes.agents, |
| | ); |
| | message.image_urls = image_urls.length ? image_urls : undefined; |
| | return files; |
| | } |
| |
|
| | async buildMessages( |
| | messages, |
| | parentMessageId, |
| | { instructions = null, additional_instructions = null }, |
| | opts, |
| | ) { |
| | let orderedMessages = this.constructor.getMessagesForConversation({ |
| | messages, |
| | parentMessageId, |
| | summary: this.shouldSummarize, |
| | }); |
| |
|
| | orderedMessages = applyAgentLabelsToHistory( |
| | orderedMessages, |
| | this.options.agent, |
| | this.agentConfigs, |
| | ); |
| |
|
| | let payload; |
| | |
| | let promptTokens; |
| |
|
| | |
| | let systemContent = [instructions ?? '', additional_instructions ?? ''] |
| | .filter(Boolean) |
| | .join('\n') |
| | .trim(); |
| |
|
| | if (this.options.attachments) { |
| | const attachments = await this.options.attachments; |
| | const latestMessage = orderedMessages[orderedMessages.length - 1]; |
| |
|
| | if (this.message_file_map) { |
| | this.message_file_map[latestMessage.messageId] = attachments; |
| | } else { |
| | this.message_file_map = { |
| | [latestMessage.messageId]: attachments, |
| | }; |
| | } |
| |
|
| | await this.addFileContextToMessage(latestMessage, attachments); |
| | const files = await this.processAttachments(latestMessage, attachments); |
| |
|
| | this.options.attachments = files; |
| | } |
| |
|
| | |
| | if (this.message_file_map && !isAgentsEndpoint(this.options.endpoint)) { |
| | this.contextHandlers = createContextHandlers( |
| | this.options.req, |
| | orderedMessages[orderedMessages.length - 1].text, |
| | ); |
| | } |
| |
|
| | const formattedMessages = orderedMessages.map((message, i) => { |
| | const formattedMessage = formatMessage({ |
| | message, |
| | userName: this.options?.name, |
| | assistantName: this.options?.modelLabel, |
| | }); |
| |
|
| | if (message.fileContext && i !== orderedMessages.length - 1) { |
| | if (typeof formattedMessage.content === 'string') { |
| | formattedMessage.content = message.fileContext + '\n' + formattedMessage.content; |
| | } else { |
| | const textPart = formattedMessage.content.find((part) => part.type === 'text'); |
| | textPart |
| | ? (textPart.text = message.fileContext + '\n' + textPart.text) |
| | : formattedMessage.content.unshift({ type: 'text', text: message.fileContext }); |
| | } |
| | } else if (message.fileContext && i === orderedMessages.length - 1) { |
| | systemContent = [systemContent, message.fileContext].join('\n'); |
| | } |
| |
|
| | const needsTokenCount = |
| | (this.contextStrategy && !orderedMessages[i].tokenCount) || message.fileContext; |
| |
|
| | |
| | if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) { |
| | orderedMessages[i].tokenCount = this.getTokenCountForMessage(formattedMessage); |
| | } |
| |
|
| | |
| | if (this.message_file_map && this.message_file_map[message.messageId]) { |
| | const attachments = this.message_file_map[message.messageId]; |
| | for (const file of attachments) { |
| | if (file.embedded) { |
| | this.contextHandlers?.processFile(file); |
| | continue; |
| | } |
| | if (file.metadata?.fileIdentifier) { |
| | continue; |
| | } |
| | |
| | |
| | |
| | |
| | |
| | } |
| | } |
| |
|
| | return formattedMessage; |
| | }); |
| |
|
| | if (this.contextHandlers) { |
| | this.augmentedPrompt = await this.contextHandlers.createContext(); |
| | systemContent = this.augmentedPrompt + systemContent; |
| | } |
| |
|
| | |
| | const ephemeralAgent = this.options.req.body.ephemeralAgent; |
| | let mcpServers = []; |
| |
|
| | |
| | if (ephemeralAgent && ephemeralAgent.mcp && ephemeralAgent.mcp.length > 0) { |
| | mcpServers = ephemeralAgent.mcp; |
| | } |
| | |
| | else if (this.options.agent && this.options.agent.tools) { |
| | mcpServers = this.options.agent.tools |
| | .filter( |
| | (tool) => |
| | tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter), |
| | ) |
| | .map((tool) => tool.name.split(Constants.mcp_delimiter).pop()) |
| | .filter(Boolean); |
| | } |
| |
|
| | if (mcpServers.length > 0) { |
| | try { |
| | const mcpInstructions = await getMCPManager().formatInstructionsForContext(mcpServers); |
| | if (mcpInstructions) { |
| | systemContent = [systemContent, mcpInstructions].filter(Boolean).join('\n\n'); |
| | logger.debug('[AgentClient] Injected MCP instructions for servers:', mcpServers); |
| | } |
| | } catch (error) { |
| | logger.error('[AgentClient] Failed to inject MCP instructions:', error); |
| | } |
| | } |
| |
|
| | if (systemContent) { |
| | this.options.agent.instructions = systemContent; |
| | } |
| |
|
| | |
| | let tokenCountMap; |
| |
|
| | if (this.contextStrategy) { |
| | ({ payload, promptTokens, tokenCountMap, messages } = await this.handleContextStrategy({ |
| | orderedMessages, |
| | formattedMessages, |
| | })); |
| | } |
| |
|
| | for (let i = 0; i < messages.length; i++) { |
| | this.indexTokenCountMap[i] = messages[i].tokenCount; |
| | } |
| |
|
| | const result = { |
| | tokenCountMap, |
| | prompt: payload, |
| | promptTokens, |
| | messages, |
| | }; |
| |
|
| | if (promptTokens >= 0 && typeof opts?.getReqData === 'function') { |
| | opts.getReqData({ promptTokens }); |
| | } |
| |
|
| | const withoutKeys = await this.useMemory(); |
| | if (withoutKeys) { |
| | systemContent += `${memoryInstructions}\n\n# Existing memory about the user:\n${withoutKeys}`; |
| | } |
| |
|
| | if (systemContent) { |
| | this.options.agent.instructions = systemContent; |
| | } |
| |
|
| | return result; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async awaitMemoryWithTimeout(memoryPromise, timeoutMs = 3000) { |
| | if (!memoryPromise) { |
| | return; |
| | } |
| |
|
| | try { |
| | const timeoutPromise = new Promise((_, reject) => |
| | setTimeout(() => reject(new Error('Memory processing timeout')), timeoutMs), |
| | ); |
| |
|
| | const attachments = await Promise.race([memoryPromise, timeoutPromise]); |
| | return attachments; |
| | } catch (error) { |
| | if (error.message === 'Memory processing timeout') { |
| | logger.warn('[AgentClient] Memory processing timed out after 3 seconds'); |
| | } else { |
| | logger.error('[AgentClient] Error processing memory:', error); |
| | } |
| | return; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async useMemory() { |
| | const user = this.options.req.user; |
| | if (user.personalization?.memories === false) { |
| | return; |
| | } |
| | const hasAccess = await checkAccess({ |
| | user, |
| | permissionType: PermissionTypes.MEMORIES, |
| | permissions: [Permissions.USE], |
| | getRoleByName, |
| | }); |
| |
|
| | if (!hasAccess) { |
| | logger.debug( |
| | `[api/server/controllers/agents/client.js #useMemory] User ${user.id} does not have USE permission for memories`, |
| | ); |
| | return; |
| | } |
| | const appConfig = this.options.req.config; |
| | const memoryConfig = appConfig.memory; |
| | if (!memoryConfig || memoryConfig.disabled === true) { |
| | return; |
| | } |
| |
|
| | |
| | let prelimAgent; |
| | const allowedProviders = new Set( |
| | appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, |
| | ); |
| | try { |
| | if (memoryConfig.agent?.id != null && memoryConfig.agent.id !== this.options.agent.id) { |
| | prelimAgent = await loadAgent({ |
| | req: this.options.req, |
| | agent_id: memoryConfig.agent.id, |
| | endpoint: EModelEndpoint.agents, |
| | }); |
| | } else if ( |
| | memoryConfig.agent?.id == null && |
| | memoryConfig.agent?.model != null && |
| | memoryConfig.agent?.provider != null |
| | ) { |
| | prelimAgent = { id: Constants.EPHEMERAL_AGENT_ID, ...memoryConfig.agent }; |
| | } |
| | } catch (error) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #useMemory] Error loading agent for memory', |
| | error, |
| | ); |
| | } |
| |
|
| | const agent = await initializeAgent({ |
| | req: this.options.req, |
| | res: this.options.res, |
| | agent: prelimAgent, |
| | allowedProviders, |
| | endpointOption: { |
| | endpoint: |
| | prelimAgent.id !== Constants.EPHEMERAL_AGENT_ID |
| | ? EModelEndpoint.agents |
| | : memoryConfig.agent?.provider, |
| | }, |
| | }); |
| |
|
| | if (!agent) { |
| | logger.warn( |
| | '[api/server/controllers/agents/client.js #useMemory] No agent found for memory', |
| | memoryConfig, |
| | ); |
| | return; |
| | } |
| |
|
| | const llmConfig = Object.assign( |
| | { |
| | provider: agent.provider, |
| | model: agent.model, |
| | }, |
| | agent.model_parameters, |
| | ); |
| |
|
| | |
| | const config = { |
| | validKeys: memoryConfig.validKeys, |
| | instructions: agent.instructions, |
| | llmConfig, |
| | tokenLimit: memoryConfig.tokenLimit, |
| | }; |
| |
|
| | const userId = this.options.req.user.id + ''; |
| | const messageId = this.responseMessageId + ''; |
| | const conversationId = this.conversationId + ''; |
| | const [withoutKeys, processMemory] = await createMemoryProcessor({ |
| | userId, |
| | config, |
| | messageId, |
| | conversationId, |
| | memoryMethods: { |
| | setMemory, |
| | deleteMemory, |
| | getFormattedMemories, |
| | }, |
| | res: this.options.res, |
| | }); |
| |
|
| | this.processMemory = processMemory; |
| | return withoutKeys; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | filterImageUrls(message) { |
| | if (!message.content || typeof message.content === 'string') { |
| | return message; |
| | } |
| |
|
| | if (Array.isArray(message.content)) { |
| | const filteredContent = message.content.filter( |
| | (part) => part.type !== ContentTypes.IMAGE_URL, |
| | ); |
| |
|
| | if (filteredContent.length === 1 && filteredContent[0].type === ContentTypes.TEXT) { |
| | const MessageClass = message.constructor; |
| | return new MessageClass({ |
| | content: filteredContent[0].text, |
| | additional_kwargs: message.additional_kwargs, |
| | }); |
| | } |
| |
|
| | const MessageClass = message.constructor; |
| | return new MessageClass({ |
| | content: filteredContent, |
| | additional_kwargs: message.additional_kwargs, |
| | }); |
| | } |
| |
|
| | return message; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async runMemory(messages) { |
| | try { |
| | if (this.processMemory == null) { |
| | return; |
| | } |
| | const appConfig = this.options.req.config; |
| | const memoryConfig = appConfig.memory; |
| | const messageWindowSize = memoryConfig?.messageWindowSize ?? 5; |
| |
|
| | let messagesToProcess = [...messages]; |
| | if (messages.length > messageWindowSize) { |
| | for (let i = messages.length - messageWindowSize; i >= 0; i--) { |
| | const potentialWindow = messages.slice(i, i + messageWindowSize); |
| | if (potentialWindow[0]?.role === 'user') { |
| | messagesToProcess = [...potentialWindow]; |
| | break; |
| | } |
| | } |
| |
|
| | if (messagesToProcess.length === messages.length) { |
| | messagesToProcess = [...messages.slice(-messageWindowSize)]; |
| | } |
| | } |
| |
|
| | const filteredMessages = messagesToProcess.map((msg) => this.filterImageUrls(msg)); |
| | const bufferString = getBufferString(filteredMessages); |
| | const bufferMessage = new HumanMessage(`# Current Chat:\n\n${bufferString}`); |
| | return await this.processMemory([bufferMessage]); |
| | } catch (error) { |
| | logger.error('Memory Agent failed to process memory', error); |
| | } |
| | } |
| |
|
| | |
| | async sendCompletion(payload, opts = {}) { |
| | await this.chatCompletion({ |
| | payload, |
| | onProgress: opts.onProgress, |
| | userMCPAuthMap: opts.userMCPAuthMap, |
| | abortController: opts.abortController, |
| | }); |
| |
|
| | const completion = filterMalformedContentParts(this.contentParts); |
| | const metadata = this.agentIdMap ? { agentIdMap: this.agentIdMap } : undefined; |
| |
|
| | return { completion, metadata }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async recordCollectedUsage({ |
| | model, |
| | balance, |
| | transactions, |
| | context = 'message', |
| | collectedUsage = this.collectedUsage, |
| | }) { |
| | if (!collectedUsage || !collectedUsage.length) { |
| | return; |
| | } |
| | const input_tokens = |
| | (collectedUsage[0]?.input_tokens || 0) + |
| | (Number(collectedUsage[0]?.input_token_details?.cache_creation) || 0) + |
| | (Number(collectedUsage[0]?.input_token_details?.cache_read) || 0); |
| |
|
| | let output_tokens = 0; |
| | let previousTokens = input_tokens; |
| | for (let i = 0; i < collectedUsage.length; i++) { |
| | const usage = collectedUsage[i]; |
| | if (!usage) { |
| | continue; |
| | } |
| |
|
| | const cache_creation = Number(usage.input_token_details?.cache_creation) || 0; |
| | const cache_read = Number(usage.input_token_details?.cache_read) || 0; |
| |
|
| | const txMetadata = { |
| | context, |
| | balance, |
| | transactions, |
| | conversationId: this.conversationId, |
| | user: this.user ?? this.options.req.user?.id, |
| | endpointTokenConfig: this.options.endpointTokenConfig, |
| | model: usage.model ?? model ?? this.model ?? this.options.agent.model_parameters.model, |
| | }; |
| |
|
| | if (i > 0) { |
| | |
| | output_tokens += |
| | (Number(usage.input_tokens) || 0) + cache_creation + cache_read - previousTokens; |
| | } |
| |
|
| | |
| | output_tokens += Number(usage.output_tokens) || 0; |
| |
|
| | |
| | previousTokens += Number(usage.output_tokens) || 0; |
| |
|
| | if (cache_creation > 0 || cache_read > 0) { |
| | spendStructuredTokens(txMetadata, { |
| | promptTokens: { |
| | input: usage.input_tokens, |
| | write: cache_creation, |
| | read: cache_read, |
| | }, |
| | completionTokens: usage.output_tokens, |
| | }).catch((err) => { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #recordCollectedUsage] Error spending structured tokens', |
| | err, |
| | ); |
| | }); |
| | continue; |
| | } |
| | spendTokens(txMetadata, { |
| | promptTokens: usage.input_tokens, |
| | completionTokens: usage.output_tokens, |
| | }).catch((err) => { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #recordCollectedUsage] Error spending tokens', |
| | err, |
| | ); |
| | }); |
| | } |
| |
|
| | this.usage = { |
| | input_tokens, |
| | output_tokens, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getStreamUsage() { |
| | return this.usage; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getTokenCountForResponse({ content }) { |
| | return this.getTokenCountForMessage({ |
| | role: 'assistant', |
| | content, |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage }) { |
| | const originalEstimate = tokenCountMap[currentMessageId] || 0; |
| |
|
| | if (!usage || typeof usage[this.inputTokensKey] !== 'number') { |
| | return originalEstimate; |
| | } |
| |
|
| | tokenCountMap[currentMessageId] = 0; |
| | const totalTokensFromMap = Object.values(tokenCountMap).reduce((sum, count) => { |
| | const numCount = Number(count); |
| | return sum + (isNaN(numCount) ? 0 : numCount); |
| | }, 0); |
| | const totalInputTokens = usage[this.inputTokensKey] ?? 0; |
| |
|
| | const currentMessageTokens = totalInputTokens - totalTokensFromMap; |
| | return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async chatCompletion({ payload, userMCPAuthMap, abortController = null }) { |
| | |
| | let config; |
| | |
| | let run; |
| | |
| | let memoryPromise; |
| | const appConfig = this.options.req.config; |
| | const balanceConfig = getBalanceConfig(appConfig); |
| | const transactionsConfig = getTransactionsConfig(appConfig); |
| | try { |
| | if (!abortController) { |
| | abortController = new AbortController(); |
| | } |
| |
|
| | |
| | const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents]; |
| |
|
| | config = { |
| | runName: 'AgentRun', |
| | configurable: { |
| | thread_id: this.conversationId, |
| | last_agent_index: this.agentConfigs?.size ?? 0, |
| | user_id: this.user ?? this.options.req.user?.id, |
| | hide_sequential_outputs: this.options.agent.hide_sequential_outputs, |
| | requestBody: { |
| | messageId: this.responseMessageId, |
| | conversationId: this.conversationId, |
| | parentMessageId: this.parentMessageId, |
| | }, |
| | user: createSafeUser(this.options.req.user), |
| | }, |
| | recursionLimit: agentsEConfig?.recursionLimit ?? 25, |
| | signal: abortController.signal, |
| | streamMode: 'values', |
| | version: 'v2', |
| | }; |
| |
|
| | const toolSet = new Set((this.options.agent.tools ?? []).map((tool) => tool && tool.name)); |
| | let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages( |
| | payload, |
| | this.indexTokenCountMap, |
| | toolSet, |
| | ); |
| |
|
| | |
| | |
| | |
| | const runAgents = async (messages) => { |
| | const agents = [this.options.agent]; |
| | if ( |
| | this.agentConfigs && |
| | this.agentConfigs.size > 0 && |
| | ((this.options.agent.edges?.length ?? 0) > 0 || |
| | (await checkCapability(this.options.req, AgentCapabilities.chain))) |
| | ) { |
| | agents.push(...this.agentConfigs.values()); |
| | } |
| |
|
| | if (agents[0].recursion_limit && typeof agents[0].recursion_limit === 'number') { |
| | config.recursionLimit = agents[0].recursion_limit; |
| | } |
| |
|
| | if ( |
| | agentsEConfig?.maxRecursionLimit && |
| | config.recursionLimit > agentsEConfig?.maxRecursionLimit |
| | ) { |
| | config.recursionLimit = agentsEConfig?.maxRecursionLimit; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | memoryPromise = this.runMemory(messages); |
| |
|
| | run = await createRun({ |
| | agents, |
| | indexTokenCountMap, |
| | runId: this.responseMessageId, |
| | signal: abortController.signal, |
| | customHandlers: this.options.eventHandlers, |
| | requestBody: config.configurable.requestBody, |
| | user: createSafeUser(this.options.req?.user), |
| | tokenCounter: createTokenCounter(this.getEncoding()), |
| | }); |
| |
|
| | if (!run) { |
| | throw new Error('Failed to create run'); |
| | } |
| |
|
| | this.run = run; |
| | if (userMCPAuthMap != null) { |
| | config.configurable.userMCPAuthMap = userMCPAuthMap; |
| | } |
| |
|
| | |
| | config.configurable.last_agent_id = agents[agents.length - 1].id; |
| | await run.processStream({ messages }, config, { |
| | callbacks: { |
| | [Callback.TOOL_ERROR]: logToolError, |
| | }, |
| | }); |
| |
|
| | config.signal = null; |
| | }; |
| |
|
| | await runAgents(initialMessages); |
| | |
| | if (config.configurable.hide_sequential_outputs) { |
| | this.contentParts = this.contentParts.filter((part, index) => { |
| | |
| | |
| | |
| | |
| | return ( |
| | index >= this.contentParts.length - 1 || |
| | part.type === ContentTypes.TOOL_CALL || |
| | part.tool_call_ids |
| | ); |
| | }); |
| | } |
| |
|
| | try { |
| | |
| | const shouldStoreAgentMap = |
| | (this.options.agent.edges?.length ?? 0) > 0 || (this.agentConfigs?.size ?? 0) > 0; |
| | if (shouldStoreAgentMap && run?.Graph) { |
| | const contentPartAgentMap = run.Graph.getContentPartAgentMap(); |
| | if (contentPartAgentMap && contentPartAgentMap.size > 0) { |
| | this.agentIdMap = Object.fromEntries(contentPartAgentMap); |
| | logger.debug('[AgentClient] Captured agent ID map:', { |
| | totalParts: this.contentParts.length, |
| | mappedParts: Object.keys(this.agentIdMap).length, |
| | }); |
| | } |
| | } |
| | } catch (error) { |
| | logger.error('[AgentClient] Error capturing agent ID map:', error); |
| | } |
| | } catch (err) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #sendCompletion] Operation aborted', |
| | err, |
| | ); |
| | if (!abortController.signal.aborted) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type', |
| | err, |
| | ); |
| | this.contentParts.push({ |
| | type: ContentTypes.ERROR, |
| | [ContentTypes.ERROR]: `An error occurred while processing the request${err?.message ? `: ${err.message}` : ''}`, |
| | }); |
| | } |
| | } finally { |
| | try { |
| | const attachments = await this.awaitMemoryWithTimeout(memoryPromise); |
| | if (attachments && attachments.length > 0) { |
| | this.artifactPromises.push(...attachments); |
| | } |
| |
|
| | await this.recordCollectedUsage({ |
| | context: 'message', |
| | balance: balanceConfig, |
| | transactions: transactionsConfig, |
| | }); |
| | } catch (err) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #chatCompletion] Error in cleanup phase', |
| | err, |
| | ); |
| | } |
| | run = null; |
| | config = null; |
| | memoryPromise = null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async titleConvo({ text, abortController }) { |
| | if (!this.run) { |
| | throw new Error('Run not initialized'); |
| | } |
| | const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator(); |
| | const { req, res, agent } = this.options; |
| | const appConfig = req.config; |
| | let endpoint = agent.endpoint; |
| |
|
| | |
| | let clientOptions = { |
| | model: agent.model || agent.model_parameters.model, |
| | }; |
| |
|
| | let titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig }); |
| |
|
| | |
| | const endpointConfig = |
| | appConfig.endpoints?.all ?? |
| | appConfig.endpoints?.[endpoint] ?? |
| | titleProviderConfig.customEndpointConfig; |
| | if (!endpointConfig) { |
| | logger.debug( |
| | `[api/server/controllers/agents/client.js #titleConvo] No endpoint config for "${endpoint}"`, |
| | ); |
| | } |
| |
|
| | if (endpointConfig?.titleConvo === false) { |
| | logger.debug( |
| | `[api/server/controllers/agents/client.js #titleConvo] Title generation disabled for endpoint "${endpoint}"`, |
| | ); |
| | return; |
| | } |
| |
|
| | if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) { |
| | try { |
| | titleProviderConfig = getProviderConfig({ |
| | provider: endpointConfig.titleEndpoint, |
| | appConfig, |
| | }); |
| | endpoint = endpointConfig.titleEndpoint; |
| | } catch (error) { |
| | logger.warn( |
| | `[api/server/controllers/agents/client.js #titleConvo] Error getting title endpoint config for "${endpointConfig.titleEndpoint}", falling back to default`, |
| | error, |
| | ); |
| | |
| | endpoint = agent.endpoint; |
| | titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig }); |
| | } |
| | } |
| |
|
| | if ( |
| | endpointConfig && |
| | endpointConfig.titleModel && |
| | endpointConfig.titleModel !== Constants.CURRENT_MODEL |
| | ) { |
| | clientOptions.model = endpointConfig.titleModel; |
| | } |
| |
|
| | const options = await titleProviderConfig.getOptions({ |
| | req, |
| | res, |
| | optionsOnly: true, |
| | overrideEndpoint: endpoint, |
| | overrideModel: clientOptions.model, |
| | endpointOption: { model_parameters: clientOptions }, |
| | }); |
| |
|
| | let provider = options.provider ?? titleProviderConfig.overrideProvider ?? agent.provider; |
| | if ( |
| | endpoint === EModelEndpoint.azureOpenAI && |
| | options.llmConfig?.azureOpenAIApiInstanceName == null |
| | ) { |
| | provider = Providers.OPENAI; |
| | } else if ( |
| | endpoint === EModelEndpoint.azureOpenAI && |
| | options.llmConfig?.azureOpenAIApiInstanceName != null && |
| | provider !== Providers.AZURE |
| | ) { |
| | provider = Providers.AZURE; |
| | } |
| |
|
| | |
| | clientOptions = { ...options.llmConfig }; |
| | if (options.configOptions) { |
| | clientOptions.configuration = options.configOptions; |
| | } |
| |
|
| | if (clientOptions.maxTokens != null) { |
| | delete clientOptions.maxTokens; |
| | } |
| | if (clientOptions?.modelKwargs?.max_completion_tokens != null) { |
| | delete clientOptions.modelKwargs.max_completion_tokens; |
| | } |
| | if (clientOptions?.modelKwargs?.max_output_tokens != null) { |
| | delete clientOptions.modelKwargs.max_output_tokens; |
| | } |
| |
|
| | clientOptions = Object.assign( |
| | Object.fromEntries( |
| | Object.entries(clientOptions).filter(([key]) => !omitTitleOptions.has(key)), |
| | ), |
| | ); |
| |
|
| | if ( |
| | provider === Providers.GOOGLE && |
| | (endpointConfig?.titleMethod === TitleMethod.FUNCTIONS || |
| | endpointConfig?.titleMethod === TitleMethod.STRUCTURED) |
| | ) { |
| | clientOptions.json = true; |
| | } |
| |
|
| | |
| | |
| | |
| | if (clientOptions?.configuration?.defaultHeaders != null) { |
| | clientOptions.configuration.defaultHeaders = resolveHeaders({ |
| | headers: clientOptions.configuration.defaultHeaders, |
| | user: createSafeUser(this.options.req?.user), |
| | body: { |
| | messageId: this.responseMessageId, |
| | conversationId: this.conversationId, |
| | parentMessageId: this.parentMessageId, |
| | }, |
| | }); |
| | } |
| |
|
| | try { |
| | const titleResult = await this.run.generateTitle({ |
| | provider, |
| | clientOptions, |
| | inputText: text, |
| | contentParts: this.contentParts, |
| | titleMethod: endpointConfig?.titleMethod, |
| | titlePrompt: endpointConfig?.titlePrompt, |
| | titlePromptTemplate: endpointConfig?.titlePromptTemplate, |
| | chainOptions: { |
| | signal: abortController.signal, |
| | callbacks: [ |
| | { |
| | handleLLMEnd, |
| | }, |
| | ], |
| | configurable: { |
| | thread_id: this.conversationId, |
| | user_id: this.user ?? this.options.req.user?.id, |
| | }, |
| | }, |
| | }); |
| |
|
| | const collectedUsage = collectedMetadata.map((item) => { |
| | let input_tokens, output_tokens; |
| |
|
| | if (item.usage) { |
| | input_tokens = |
| | item.usage.prompt_tokens || item.usage.input_tokens || item.usage.inputTokens; |
| | output_tokens = |
| | item.usage.completion_tokens || item.usage.output_tokens || item.usage.outputTokens; |
| | } else if (item.tokenUsage) { |
| | input_tokens = item.tokenUsage.promptTokens; |
| | output_tokens = item.tokenUsage.completionTokens; |
| | } |
| |
|
| | return { |
| | input_tokens: input_tokens, |
| | output_tokens: output_tokens, |
| | }; |
| | }); |
| |
|
| | const balanceConfig = getBalanceConfig(appConfig); |
| | const transactionsConfig = getTransactionsConfig(appConfig); |
| | await this.recordCollectedUsage({ |
| | collectedUsage, |
| | context: 'title', |
| | model: clientOptions.model, |
| | balance: balanceConfig, |
| | transactions: transactionsConfig, |
| | }).catch((err) => { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage', |
| | err, |
| | ); |
| | }); |
| |
|
| | return sanitizeTitle(titleResult.title); |
| | } catch (err) { |
| | logger.error('[api/server/controllers/agents/client.js #titleConvo] Error', err); |
| | return; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async recordTokenUsage({ |
| | model, |
| | usage, |
| | balance, |
| | promptTokens, |
| | completionTokens, |
| | context = 'message', |
| | }) { |
| | try { |
| | await spendTokens( |
| | { |
| | model, |
| | context, |
| | balance, |
| | conversationId: this.conversationId, |
| | user: this.user ?? this.options.req.user?.id, |
| | endpointTokenConfig: this.options.endpointTokenConfig, |
| | }, |
| | { promptTokens, completionTokens }, |
| | ); |
| |
|
| | if ( |
| | usage && |
| | typeof usage === 'object' && |
| | 'reasoning_tokens' in usage && |
| | typeof usage.reasoning_tokens === 'number' |
| | ) { |
| | await spendTokens( |
| | { |
| | model, |
| | balance, |
| | context: 'reasoning', |
| | conversationId: this.conversationId, |
| | user: this.user ?? this.options.req.user?.id, |
| | endpointTokenConfig: this.options.endpointTokenConfig, |
| | }, |
| | { completionTokens: usage.reasoning_tokens }, |
| | ); |
| | } |
| | } catch (error) { |
| | logger.error( |
| | '[api/server/controllers/agents/client.js #recordTokenUsage] Error recording token usage', |
| | error, |
| | ); |
| | } |
| | } |
| |
|
| | getEncoding() { |
| | return 'o200k_base'; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | getTokenCount(text) { |
| | const encoding = this.getEncoding(); |
| | return Tokenizer.getTokenCount(text, encoding); |
| | } |
| | } |
| |
|
| | module.exports = AgentClient; |
| |
|