| | const { logger } = require('@librechat/data-schemas'); |
| | const { Constants } = require('librechat-data-provider'); |
| | const { |
| | sendEvent, |
| | sanitizeFileForTransmit, |
| | sanitizeMessageForTransmit, |
| | } = require('@librechat/api'); |
| | const { |
| | handleAbortError, |
| | createAbortController, |
| | cleanupAbortController, |
| | } = require('~/server/middleware'); |
| | const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup'); |
| | const { saveMessage } = require('~/models'); |
| |
|
| | function createCloseHandler(abortController) { |
| | return function (manual) { |
| | if (!manual) { |
| | logger.debug('[AgentController] Request closed'); |
| | } |
| | if (!abortController) { |
| | return; |
| | } else if (abortController.signal.aborted) { |
| | return; |
| | } else if (abortController.requestCompleted) { |
| | return; |
| | } |
| |
|
| | abortController.abort(); |
| | logger.debug('[AgentController] Request aborted on close'); |
| | }; |
| | } |
| |
|
| | const AgentController = async (req, res, next, initializeClient, addTitle) => { |
| | let { |
| | text, |
| | isRegenerate, |
| | endpointOption, |
| | conversationId, |
| | isContinued = false, |
| | editedContent = null, |
| | parentMessageId = null, |
| | overrideParentMessageId = null, |
| | responseMessageId: editedResponseMessageId = null, |
| | } = req.body; |
| |
|
| | let sender; |
| | let abortKey; |
| | let userMessage; |
| | let promptTokens; |
| | let userMessageId; |
| | let responseMessageId; |
| | let userMessagePromise; |
| | let getAbortData; |
| | let client = null; |
| | let cleanupHandlers = []; |
| |
|
| | const newConvo = !conversationId; |
| | const userId = req.user.id; |
| |
|
| | |
| | let getReqData = (data = {}) => { |
| | for (let key in data) { |
| | if (key === 'userMessage') { |
| | userMessage = data[key]; |
| | userMessageId = data[key].messageId; |
| | } else if (key === 'userMessagePromise') { |
| | userMessagePromise = data[key]; |
| | } else if (key === 'responseMessageId') { |
| | responseMessageId = data[key]; |
| | } else if (key === 'promptTokens') { |
| | promptTokens = data[key]; |
| | } else if (key === 'sender') { |
| | sender = data[key]; |
| | } else if (key === 'abortKey') { |
| | abortKey = data[key]; |
| | } else if (!conversationId && key === 'conversationId') { |
| | conversationId = data[key]; |
| | } |
| | } |
| | }; |
| |
|
| | |
| | const performCleanup = () => { |
| | logger.debug('[AgentController] Performing cleanup'); |
| | if (Array.isArray(cleanupHandlers)) { |
| | for (const handler of cleanupHandlers) { |
| | try { |
| | if (typeof handler === 'function') { |
| | handler(); |
| | } |
| | } catch (e) { |
| | logger.error('[AgentController] Error in cleanup handler', e); |
| | } |
| | } |
| | } |
| |
|
| | |
| | if (abortKey) { |
| | logger.debug('[AgentController] Cleaning up abort controller'); |
| | cleanupAbortController(abortKey); |
| | } |
| |
|
| | |
| | if (client) { |
| | disposeClient(client); |
| | } |
| |
|
| | |
| | client = null; |
| | getReqData = null; |
| | userMessage = null; |
| | getAbortData = null; |
| | endpointOption.agent = null; |
| | endpointOption = null; |
| | cleanupHandlers = null; |
| | userMessagePromise = null; |
| |
|
| | |
| | if (requestDataMap.has(req)) { |
| | requestDataMap.delete(req); |
| | } |
| | logger.debug('[AgentController] Cleanup completed'); |
| | }; |
| |
|
| | try { |
| | let prelimAbortController = new AbortController(); |
| | const prelimCloseHandler = createCloseHandler(prelimAbortController); |
| | res.on('close', prelimCloseHandler); |
| | const removePrelimHandler = (manual) => { |
| | try { |
| | prelimCloseHandler(manual); |
| | res.removeListener('close', prelimCloseHandler); |
| | } catch (e) { |
| | logger.error('[AgentController] Error removing close listener', e); |
| | } |
| | }; |
| | cleanupHandlers.push(removePrelimHandler); |
| | |
| | const result = await initializeClient({ |
| | req, |
| | res, |
| | endpointOption, |
| | signal: prelimAbortController.signal, |
| | }); |
| | if (prelimAbortController.signal?.aborted) { |
| | prelimAbortController = null; |
| | throw new Error('Request was aborted before initialization could complete'); |
| | } else { |
| | prelimAbortController = null; |
| | removePrelimHandler(true); |
| | cleanupHandlers.pop(); |
| | } |
| | client = result.client; |
| |
|
| | |
| | if (clientRegistry) { |
| | clientRegistry.register(client, { userId }, client); |
| | } |
| |
|
| | |
| | requestDataMap.set(req, { client }); |
| |
|
| | |
| | const contentRef = new WeakRef(client.contentParts || []); |
| |
|
| | |
| | getAbortData = () => { |
| | |
| | const content = contentRef.deref(); |
| |
|
| | return { |
| | sender, |
| | content: content || [], |
| | userMessage, |
| | promptTokens, |
| | conversationId, |
| | userMessagePromise, |
| | messageId: responseMessageId, |
| | parentMessageId: overrideParentMessageId ?? userMessageId, |
| | }; |
| | }; |
| |
|
| | const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData); |
| | const closeHandler = createCloseHandler(abortController); |
| | res.on('close', closeHandler); |
| | cleanupHandlers.push(() => { |
| | try { |
| | res.removeListener('close', closeHandler); |
| | } catch (e) { |
| | logger.error('[AgentController] Error removing close listener', e); |
| | } |
| | }); |
| |
|
| | const messageOptions = { |
| | user: userId, |
| | onStart, |
| | getReqData, |
| | isContinued, |
| | isRegenerate, |
| | editedContent, |
| | conversationId, |
| | parentMessageId, |
| | abortController, |
| | overrideParentMessageId, |
| | isEdited: !!editedContent, |
| | userMCPAuthMap: result.userMCPAuthMap, |
| | responseMessageId: editedResponseMessageId, |
| | progressOptions: { |
| | res, |
| | }, |
| | }; |
| |
|
| | let response = await client.sendMessage(text, messageOptions); |
| |
|
| | |
| | const messageId = response.messageId; |
| | const endpoint = endpointOption.endpoint; |
| | response.endpoint = endpoint; |
| |
|
| | |
| | const databasePromise = response.databasePromise; |
| | delete response.databasePromise; |
| |
|
| | |
| | const { conversation: convoData = {} } = await databasePromise; |
| | const conversation = { ...convoData }; |
| | conversation.title = |
| | conversation && !conversation.title ? null : conversation?.title || 'New Chat'; |
| |
|
| | |
| | if (req.body.files && client.options?.attachments) { |
| | userMessage.files = []; |
| | const messageFiles = new Set(req.body.files.map((file) => file.file_id)); |
| | for (const attachment of client.options.attachments) { |
| | if (messageFiles.has(attachment.file_id)) { |
| | userMessage.files.push(sanitizeFileForTransmit(attachment)); |
| | } |
| | } |
| | delete userMessage.image_urls; |
| | } |
| |
|
| | |
| | if (!abortController.signal.aborted) { |
| | |
| | const finalResponse = { ...response }; |
| |
|
| | sendEvent(res, { |
| | final: true, |
| | conversation, |
| | title: conversation.title, |
| | requestMessage: sanitizeMessageForTransmit(userMessage), |
| | responseMessage: finalResponse, |
| | }); |
| | res.end(); |
| |
|
| | |
| | if (client.savedMessageIds && !client.savedMessageIds.has(messageId)) { |
| | await saveMessage( |
| | req, |
| | { ...finalResponse, user: userId }, |
| | { context: 'api/server/controllers/agents/request.js - response end' }, |
| | ); |
| | } |
| | } |
| | |
| | |
| | else if (!res.headersSent && !res.finished) { |
| | logger.debug( |
| | '[AgentController] Handling edge case: `sendMessage` completed but aborted during `sendCompletion`', |
| | ); |
| |
|
| | const finalResponse = { ...response }; |
| | finalResponse.error = true; |
| |
|
| | sendEvent(res, { |
| | final: true, |
| | conversation, |
| | title: conversation.title, |
| | requestMessage: sanitizeMessageForTransmit(userMessage), |
| | responseMessage: finalResponse, |
| | error: { message: 'Request was aborted during completion' }, |
| | }); |
| | res.end(); |
| | } |
| |
|
| | |
| | if (!client.skipSaveUserMessage) { |
| | await saveMessage(req, userMessage, { |
| | context: "api/server/controllers/agents/request.js - don't skip saving user message", |
| | }); |
| | } |
| |
|
| | |
| | if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) { |
| | addTitle(req, { |
| | text, |
| | response: { ...response }, |
| | client, |
| | }) |
| | .then(() => { |
| | logger.debug('[AgentController] Title generation started'); |
| | }) |
| | .catch((err) => { |
| | logger.error('[AgentController] Error in title generation', err); |
| | }) |
| | .finally(() => { |
| | logger.debug('[AgentController] Title generation completed'); |
| | performCleanup(); |
| | }); |
| | } else { |
| | performCleanup(); |
| | } |
| | } catch (error) { |
| | |
| | handleAbortError(res, req, error, { |
| | conversationId, |
| | sender, |
| | messageId: responseMessageId, |
| | parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId, |
| | userMessageId, |
| | }) |
| | .catch((err) => { |
| | logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err); |
| | }) |
| | .finally(() => { |
| | performCleanup(); |
| | }); |
| | } |
| | }; |
| |
|
| | module.exports = AgentController; |
| |
|