| 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; |
|
|