mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00

* wip: Add Instructions component for agent configuration
* ✨ feat: Implement DropdownPopup for variable insertion in instructions
* refactor: Enhance variable handling by exporting specialVariables and updating Markdown components
* feat: Add special variable support for current date and user in Instructions component
* refactor: Update handleAddVariable to include localized label
* feat: replace special variables in instructions presets
* chore: update parameter type for user in getListAgents function
* refactor: integrate dayjs for date handling and move replaceSpecialVars function to data-provider
* feat: enhance replaceSpecialVars to include day number in current date format
* feat: integrate replaceSpecialVars for processing agent instructions
* feat: add support for current date & time in replaceSpecialVars function
* feat: add iso_datetime support in replaceSpecialVars function
* fix: enforce text parameter to be a required field in replaceSpecialVars function
* feat: add ISO datetime support in translation file
* fix: disable eslint warning for autoFocus in TextareaAutosize component
* feat: add VariablesDropdown component and integrate it into CreatePromptForm and PromptEditor; update translation for special variables
* fix: CategorySelector and related localizations
* fix: add z-index class to LanguageSTTDropdown for proper stacking context
* fix: add max-height and overflow styles to OGDialogContent in VariableDialog and PreviewPrompt components
* fix: update variable detection logic to exclude special variables and improve regex matching
* fix: improve accessibility text for actions menu in ChatGroupItem component
* fix: adjust max-width and height styles for dialog components and improve markdown rendering for light vs. dark, height/widths, etc.
* fix: remove commented-out code for better readability in PromptVariableGfm component
* fix: handle undefined input parameter in setParams function call
* fix: update variable label types to use TSpecialVarLabel for consistency
* fix: remove outdated information from special variables description in translation file
* fix: enhance unused i18next keys detection for special variable keys
* fix: update color classes for consistency/a11y in category and prompt variable components
* fix: update PromptVariableGfm component and special variable styles for consistency
* fix: improve variable highlighting logic in VariableForm component
* fix: update background color classes for consistency in VariableForm component
* fix: add missing ref parameter to Dialog component in OriginalDialog
* refactor: move navigate call for new conversation to after setConversation update
* refactor: move message query hook to client workspace; fix: handle edge case for navigation from finalHandler creating race condition for response message DB save
* chore: bump librechat-data-provider to 0.7.793
* ci: add unit tests for replaceSpecialVars function
* fix: implement getToolkitKey function for image_gen_oai toolkit filtering/including
* ci: enhance dayjs mock for consistent date/time values in tests
* fix: MCP stdio server fail to start when passing env property
* fix: use optional chaining for clientRef dereferencing in AskController and EditController
feat: add context to saveMessage call in streamResponse utility
* fix: only save error messages if the userMessageId was initialized
* refactor: add isNotAppendable check to disable inputs in ChatForm and useTextarea
* feat: enhance error handling in useEventHandlers and update conversation state in useNewConvo
* refactor: prepend underscore to conversationId in newConversation template
* feat: log aborted conversations with minimal messages and use consistent conversationId generation
---------
Co-authored-by: Olivier Schiavo <olivier.schiavo@wengo.com>
Co-authored-by: aka012 <aka012@neowiz.com>
Co-authored-by: jiasheng <jiashengguo@outlook.com>
273 lines
7.7 KiB
JavaScript
273 lines
7.7 KiB
JavaScript
const { Constants } = require('librechat-data-provider');
|
|
const {
|
|
handleAbortError,
|
|
createAbortController,
|
|
cleanupAbortController,
|
|
} = require('~/server/middleware');
|
|
const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup');
|
|
const { sendMessage } = require('~/server/utils');
|
|
const { saveMessage } = require('~/models');
|
|
const { logger } = require('~/config');
|
|
|
|
const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
|
let {
|
|
text,
|
|
endpointOption,
|
|
conversationId,
|
|
parentMessageId = null,
|
|
overrideParentMessageId = null,
|
|
} = req.body;
|
|
|
|
let sender;
|
|
let abortKey;
|
|
let userMessage;
|
|
let promptTokens;
|
|
let userMessageId;
|
|
let responseMessageId;
|
|
let userMessagePromise;
|
|
let getAbortData;
|
|
let client = null;
|
|
// Initialize as an array
|
|
let cleanupHandlers = [];
|
|
|
|
const newConvo = !conversationId;
|
|
const userId = req.user.id;
|
|
|
|
// Create handler to avoid capturing the entire parent scope
|
|
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];
|
|
}
|
|
}
|
|
};
|
|
|
|
// Create a function to handle final cleanup
|
|
const performCleanup = () => {
|
|
logger.debug('[AgentController] Performing cleanup');
|
|
// Make sure cleanupHandlers is an array before iterating
|
|
if (Array.isArray(cleanupHandlers)) {
|
|
// Execute all cleanup handlers
|
|
for (const handler of cleanupHandlers) {
|
|
try {
|
|
if (typeof handler === 'function') {
|
|
handler();
|
|
}
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up abort controller
|
|
if (abortKey) {
|
|
logger.debug('[AgentController] Cleaning up abort controller');
|
|
cleanupAbortController(abortKey);
|
|
}
|
|
|
|
// Dispose client properly
|
|
if (client) {
|
|
disposeClient(client);
|
|
}
|
|
|
|
// Clear all references
|
|
client = null;
|
|
getReqData = null;
|
|
userMessage = null;
|
|
getAbortData = null;
|
|
endpointOption.agent = null;
|
|
endpointOption = null;
|
|
cleanupHandlers = null;
|
|
userMessagePromise = null;
|
|
|
|
// Clear request data map
|
|
if (requestDataMap.has(req)) {
|
|
requestDataMap.delete(req);
|
|
}
|
|
logger.debug('[AgentController] Cleanup completed');
|
|
};
|
|
|
|
try {
|
|
/** @type {{ client: TAgentClient }} */
|
|
const result = await initializeClient({ req, res, endpointOption });
|
|
client = result.client;
|
|
|
|
// Register client with finalization registry if available
|
|
if (clientRegistry) {
|
|
clientRegistry.register(client, { userId }, client);
|
|
}
|
|
|
|
// Store request data in WeakMap keyed by req object
|
|
requestDataMap.set(req, { client });
|
|
|
|
// Use WeakRef to allow GC but still access content if it exists
|
|
const contentRef = new WeakRef(client.contentParts || []);
|
|
|
|
// Minimize closure scope - only capture small primitives and WeakRef
|
|
getAbortData = () => {
|
|
// Dereference WeakRef each time
|
|
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);
|
|
|
|
// Simple handler to avoid capturing scope
|
|
const closeHandler = () => {
|
|
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');
|
|
};
|
|
|
|
res.on('close', closeHandler);
|
|
cleanupHandlers.push(() => {
|
|
try {
|
|
res.removeListener('close', closeHandler);
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
});
|
|
|
|
const messageOptions = {
|
|
user: userId,
|
|
onStart,
|
|
getReqData,
|
|
conversationId,
|
|
parentMessageId,
|
|
abortController,
|
|
overrideParentMessageId,
|
|
progressOptions: {
|
|
res,
|
|
},
|
|
};
|
|
|
|
let response = await client.sendMessage(text, messageOptions);
|
|
|
|
// Extract what we need and immediately break reference
|
|
const messageId = response.messageId;
|
|
const endpoint = endpointOption.endpoint;
|
|
response.endpoint = endpoint;
|
|
|
|
// Store database promise locally
|
|
const databasePromise = response.databasePromise;
|
|
delete response.databasePromise;
|
|
|
|
// Resolve database-related data
|
|
const { conversation: convoData = {} } = await databasePromise;
|
|
const conversation = { ...convoData };
|
|
conversation.title =
|
|
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
|
|
|
// Process files if needed
|
|
if (req.body.files && client.options?.attachments) {
|
|
userMessage.files = [];
|
|
const messageFiles = new Set(req.body.files.map((file) => file.file_id));
|
|
for (let attachment of client.options.attachments) {
|
|
if (messageFiles.has(attachment.file_id)) {
|
|
userMessage.files.push({ ...attachment });
|
|
}
|
|
}
|
|
delete userMessage.image_urls;
|
|
}
|
|
|
|
// Only send if not aborted
|
|
if (!abortController.signal.aborted) {
|
|
// Create a new response object with minimal copies
|
|
const finalResponse = { ...response };
|
|
|
|
sendMessage(res, {
|
|
final: true,
|
|
conversation,
|
|
title: conversation.title,
|
|
requestMessage: userMessage,
|
|
responseMessage: finalResponse,
|
|
});
|
|
res.end();
|
|
|
|
// Save the message if needed
|
|
if (client.savedMessageIds && !client.savedMessageIds.has(messageId)) {
|
|
await saveMessage(
|
|
req,
|
|
{ ...finalResponse, user: userId },
|
|
{ context: 'api/server/controllers/agents/request.js - response end' },
|
|
);
|
|
}
|
|
}
|
|
|
|
// Save user message if needed
|
|
if (!client.skipSaveUserMessage) {
|
|
await saveMessage(req, userMessage, {
|
|
context: 'api/server/controllers/agents/request.js - don\'t skip saving user message',
|
|
});
|
|
}
|
|
|
|
// Add title if needed - extract minimal data
|
|
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) {
|
|
// Handle error without capturing much scope
|
|
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;
|