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