mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-19 14:16:34 +01:00
* ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * ✨ feat: implement cursor-based pagination for conversations API * 🔧 refactor: remove createdAt from conversation selection in API and type definitions * 🔧 refactor: include createdAt in conversation selection and update related types * ✨ fix: search functionality and bugs with loadMoreConversations * feat: move ArchivedChats to cursor and DataTable standard * 🔧 refactor: add InfiniteQueryObserverResult type import in Nav component * feat: enhance conversation listing with pagination, sorting, and search capabilities * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: remove unused translation keys for archived chats and search results * 🔧 fix: Archived Chats, Delete Convo, Duplicate Convo * 🔧 refactor: improve conversation components with layout adjustments and new translations * 🔧 refactor: simplify archive conversation mutation and improve unarchive handling; fix: update fork mutation * 🔧 refactor: decode search query parameter in conversation route; improve error handling in unarchive mutation; clean up DataTable component styles * 🔧 refactor: remove unused translation key for empty archived chats * 🚀 fix: `archivedConversation` query key not updated correctly while archiving * 🧠 feat: Bedrock Anthropic Reasoning & Update Endpoint Handling (#6163) * feat: Add thinking and thinkingBudget parameters for Bedrock Anthropic models * chore: Update @librechat/agents to version 2.1.8 * refactor: change region order in params * refactor: Add maxTokens parameter to conversation preset schema * refactor: Update agent client to use bedrockInputSchema and improve error handling for model parameters * refactor: streamline/optimize llmConfig initialization and saving for bedrock * fix: ensure config titleModel is used for all endpoints * refactor: enhance OpenAIClient and agent initialization to support endpoint checks for OpenRouter * chore: bump @google/generative-ai * ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: update translation keys for clarity; simplify conversation query parameters and improve sorting functionality in SharedLinks component * 🔧 refactor: optimize conversation loading logic and improve search handling in Nav component * fix: package-lock * fix: package-lock 2 * fix: package lock 3 * refactor: remove unused utility files and exports to clean up the codebase * refactor: remove i18n and useAuthRedirect modules to streamline codebase * refactor: optimize Conversations component and remove unused ToggleContext * refactor(Convo): add RenameForm and ConvoLink components; enhance Conversations component with responsive design * fix: add missing @azure/storage-blob dependency in package.json * refactor(Search): add error handling with toast notification for search errors * refactor: make createdAt and updatedAt fields of tConvoUpdateSchema less restrictive if timestamps are missing * chore: update @azure/storage-blob dependency to version 12.27.0, ensure package-lock is correct * refactor(Search): improve conversation handling server side * fix: eslint warning and errors * refactor(Search): improved search loading state and overall UX * Refactors conversation cache management Centralizes conversation mutation logic into dedicated utility functions for adding, updating, and removing conversations from query caches. Improves reliability and maintainability by: - Consolidating duplicate cache manipulation code - Adding type safety for infinite query data structures - Implementing consistent cache update patterns across all conversation operations - Removing obsolete conversation helper functions in favor of standardized utilities * fix: conversation handling and SSE event processing - Optimizes conversation state management with useMemo and proper hook ordering - Improves SSE event handler documentation and error handling - Adds reset guard flag for conversation changes - Removes redundant navigation call - Cleans up cursor handling logic and document structure Improves code maintainability and prevents potential race conditions in conversation state updates * refactor: add type for SearchBar `onChange` * fix: type tags * style: rounded to xl all Header buttons * fix: activeConvo in Convo not working * style(Bookmarks): improved UI * a11y(AccountSettings): fixed hover style not visible when using light theme * style(SettingsTabs): improved tab switchers and dropdowns * feat: add translations keys for Speech * chore: fix package-lock * fix(mutations): legacy import after rebase * feat: refactor conversation navigation for accessibility * fix(search): convo and message create/update date not returned * fix(search): show correct iconURL and endpoint for searched messages * fix: small UI improvements * chore: console.log cleanup * chore: fix tests * fix(ChatForm): improve conversation ID handling and clean up useMemo dependencies * chore: improve typing * chore: improve typing * fix(useSSE): clear conversation ID on submission to prevent draft restoration * refactor(OpenAIClient): clean up abort handler * refactor(abortMiddleware): change handleAbort to use function expression * feat: add PENDING_CONVO constant and update conversation ID checks * fix: final event handling on abort * fix: improve title sync and query cache sync on final event * fix: prevent overwriting cached conversation data if it already exists --------- Co-authored-by: Danny Avila <danny@librechat.ai>
309 lines
10 KiB
JavaScript
309 lines
10 KiB
JavaScript
const Conversation = require('./schema/convoSchema');
|
|
const { getMessages, deleteMessages } = require('./Message');
|
|
const logger = require('~/config/winston');
|
|
|
|
/**
|
|
* Searches for a conversation by conversationId and returns a lean document with only conversationId and user.
|
|
* @param {string} conversationId - The conversation's ID.
|
|
* @returns {Promise<{conversationId: string, user: string} | null>} The conversation object with selected fields or null if not found.
|
|
*/
|
|
const searchConversation = async (conversationId) => {
|
|
try {
|
|
return await Conversation.findOne({ conversationId }, 'conversationId user').lean();
|
|
} catch (error) {
|
|
logger.error('[searchConversation] Error searching conversation', error);
|
|
throw new Error('Error searching conversation');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrieves a single conversation for a given user and conversation ID.
|
|
* @param {string} user - The user's ID.
|
|
* @param {string} conversationId - The conversation's ID.
|
|
* @returns {Promise<TConversation>} The conversation object.
|
|
*/
|
|
const getConvo = async (user, conversationId) => {
|
|
try {
|
|
return await Conversation.findOne({ user, conversationId }).lean();
|
|
} catch (error) {
|
|
logger.error('[getConvo] Error getting single conversation', error);
|
|
return { message: 'Error getting single conversation' };
|
|
}
|
|
};
|
|
|
|
const deleteNullOrEmptyConversations = async () => {
|
|
try {
|
|
const filter = {
|
|
$or: [
|
|
{ conversationId: null },
|
|
{ conversationId: '' },
|
|
{ conversationId: { $exists: false } },
|
|
],
|
|
};
|
|
|
|
const result = await Conversation.deleteMany(filter);
|
|
|
|
// Delete associated messages
|
|
const messageDeleteResult = await deleteMessages(filter);
|
|
|
|
logger.info(
|
|
`[deleteNullOrEmptyConversations] Deleted ${result.deletedCount} conversations and ${messageDeleteResult.deletedCount} messages`,
|
|
);
|
|
|
|
return {
|
|
conversations: result,
|
|
messages: messageDeleteResult,
|
|
};
|
|
} catch (error) {
|
|
logger.error('[deleteNullOrEmptyConversations] Error deleting conversations', error);
|
|
throw new Error('Error deleting conversations with null or empty conversationId');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Searches for a conversation by conversationId and returns associated file ids.
|
|
* @param {string} conversationId - The conversation's ID.
|
|
* @returns {Promise<string[] | null>}
|
|
*/
|
|
const getConvoFiles = async (conversationId) => {
|
|
try {
|
|
return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? [];
|
|
} catch (error) {
|
|
logger.error('[getConvoFiles] Error getting conversation files', error);
|
|
throw new Error('Error getting conversation files');
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
Conversation,
|
|
getConvoFiles,
|
|
searchConversation,
|
|
deleteNullOrEmptyConversations,
|
|
/**
|
|
* Saves a conversation to the database.
|
|
* @param {Object} req - The request object.
|
|
* @param {string} conversationId - The conversation's ID.
|
|
* @param {Object} metadata - Additional metadata to log for operation.
|
|
* @returns {Promise<TConversation>} The conversation object.
|
|
*/
|
|
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
|
|
try {
|
|
if (metadata?.context) {
|
|
logger.debug(`[saveConvo] ${metadata.context}`);
|
|
}
|
|
|
|
const messages = await getMessages({ conversationId }, '_id');
|
|
const update = { ...convo, messages, user: req.user.id };
|
|
|
|
if (newConversationId) {
|
|
update.conversationId = newConversationId;
|
|
}
|
|
|
|
if (req.body.isTemporary) {
|
|
const expiredAt = new Date();
|
|
expiredAt.setDate(expiredAt.getDate() + 30);
|
|
update.expiredAt = expiredAt;
|
|
} else {
|
|
update.expiredAt = null;
|
|
}
|
|
|
|
/** @type {{ $set: Partial<TConversation>; $unset?: Record<keyof TConversation, number> }} */
|
|
const updateOperation = { $set: update };
|
|
if (metadata && metadata.unsetFields && Object.keys(metadata.unsetFields).length > 0) {
|
|
updateOperation.$unset = metadata.unsetFields;
|
|
}
|
|
|
|
/** Note: the resulting Model object is necessary for Meilisearch operations */
|
|
const conversation = await Conversation.findOneAndUpdate(
|
|
{ conversationId, user: req.user.id },
|
|
updateOperation,
|
|
{
|
|
new: true,
|
|
upsert: true,
|
|
},
|
|
);
|
|
|
|
return conversation.toObject();
|
|
} catch (error) {
|
|
logger.error('[saveConvo] Error saving conversation', error);
|
|
if (metadata && metadata?.context) {
|
|
logger.info(`[saveConvo] ${metadata.context}`);
|
|
}
|
|
return { message: 'Error saving conversation' };
|
|
}
|
|
},
|
|
bulkSaveConvos: async (conversations) => {
|
|
try {
|
|
const bulkOps = conversations.map((convo) => ({
|
|
updateOne: {
|
|
filter: { conversationId: convo.conversationId, user: convo.user },
|
|
update: convo,
|
|
upsert: true,
|
|
timestamps: false,
|
|
},
|
|
}));
|
|
|
|
const result = await Conversation.bulkWrite(bulkOps);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('[saveBulkConversations] Error saving conversations in bulk', error);
|
|
throw new Error('Failed to save conversations in bulk.');
|
|
}
|
|
},
|
|
getConvosByCursor: async (
|
|
user,
|
|
{ cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {},
|
|
) => {
|
|
const filters = [{ user }];
|
|
|
|
if (isArchived) {
|
|
filters.push({ isArchived: true });
|
|
} else {
|
|
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
|
|
}
|
|
|
|
if (Array.isArray(tags) && tags.length > 0) {
|
|
filters.push({ tags: { $in: tags } });
|
|
}
|
|
|
|
filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] });
|
|
|
|
if (search) {
|
|
try {
|
|
const meiliResults = await Conversation.meiliSearch(search);
|
|
const matchingIds = Array.isArray(meiliResults.hits)
|
|
? meiliResults.hits.map((result) => result.conversationId)
|
|
: [];
|
|
if (!matchingIds.length) {
|
|
return { conversations: [], nextCursor: null };
|
|
}
|
|
filters.push({ conversationId: { $in: matchingIds } });
|
|
} catch (error) {
|
|
logger.error('[getConvosByCursor] Error during meiliSearch', error);
|
|
return { message: 'Error during meiliSearch' };
|
|
}
|
|
}
|
|
|
|
if (cursor) {
|
|
filters.push({ updatedAt: { $lt: new Date(cursor) } });
|
|
}
|
|
|
|
const query = filters.length === 1 ? filters[0] : { $and: filters };
|
|
|
|
try {
|
|
const convos = await Conversation.find(query)
|
|
.select('conversationId endpoint title createdAt updatedAt user')
|
|
.sort({ updatedAt: order === 'asc' ? 1 : -1 })
|
|
.limit(limit + 1)
|
|
.lean();
|
|
|
|
let nextCursor = null;
|
|
if (convos.length > limit) {
|
|
const lastConvo = convos.pop();
|
|
nextCursor = lastConvo.updatedAt.toISOString();
|
|
}
|
|
|
|
return { conversations: convos, nextCursor };
|
|
} catch (error) {
|
|
logger.error('[getConvosByCursor] Error getting conversations', error);
|
|
return { message: 'Error getting conversations' };
|
|
}
|
|
},
|
|
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
|
|
try {
|
|
if (!convoIds?.length) {
|
|
return { conversations: [], nextCursor: null, convoMap: {} };
|
|
}
|
|
|
|
const conversationIds = convoIds.map((convo) => convo.conversationId);
|
|
|
|
const results = await Conversation.find({
|
|
user,
|
|
conversationId: { $in: conversationIds },
|
|
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
|
|
}).lean();
|
|
|
|
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
|
|
let filtered = results;
|
|
if (cursor && cursor !== 'start') {
|
|
const cursorDate = new Date(cursor);
|
|
filtered = results.filter((convo) => new Date(convo.updatedAt) < cursorDate);
|
|
}
|
|
|
|
const limited = filtered.slice(0, limit + 1);
|
|
let nextCursor = null;
|
|
if (limited.length > limit) {
|
|
const lastConvo = limited.pop();
|
|
nextCursor = lastConvo.updatedAt.toISOString();
|
|
}
|
|
|
|
const convoMap = {};
|
|
limited.forEach((convo) => {
|
|
convoMap[convo.conversationId] = convo;
|
|
});
|
|
|
|
return { conversations: limited, nextCursor, convoMap };
|
|
} catch (error) {
|
|
logger.error('[getConvosQueried] Error getting conversations', error);
|
|
return { message: 'Error fetching conversations' };
|
|
}
|
|
},
|
|
getConvo,
|
|
/* chore: this method is not properly error handled */
|
|
getConvoTitle: async (user, conversationId) => {
|
|
try {
|
|
const convo = await getConvo(user, conversationId);
|
|
/* ChatGPT Browser was triggering error here due to convo being saved later */
|
|
if (convo && !convo.title) {
|
|
return null;
|
|
} else {
|
|
// TypeError: Cannot read properties of null (reading 'title')
|
|
return convo?.title || 'New Chat';
|
|
}
|
|
} catch (error) {
|
|
logger.error('[getConvoTitle] Error getting conversation title', error);
|
|
return { message: 'Error getting conversation title' };
|
|
}
|
|
},
|
|
/**
|
|
* Asynchronously deletes conversations and associated messages for a given user and filter.
|
|
*
|
|
* @async
|
|
* @function
|
|
* @param {string|ObjectId} user - The user's ID.
|
|
* @param {Object} filter - Additional filter criteria for the conversations to be deleted.
|
|
* @returns {Promise<{ n: number, ok: number, deletedCount: number, messages: { n: number, ok: number, deletedCount: number } }>}
|
|
* An object containing the count of deleted conversations and associated messages.
|
|
* @throws {Error} Throws an error if there's an issue with the database operations.
|
|
*
|
|
* @example
|
|
* const user = 'someUserId';
|
|
* const filter = { someField: 'someValue' };
|
|
* const result = await deleteConvos(user, filter);
|
|
* logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
|
|
*/
|
|
deleteConvos: async (user, filter) => {
|
|
try {
|
|
const userFilter = { ...filter, user };
|
|
|
|
const conversations = await Conversation.find(userFilter).select('conversationId');
|
|
const conversationIds = conversations.map((c) => c.conversationId);
|
|
|
|
if (!conversationIds.length) {
|
|
throw new Error('Conversation not found or already deleted.');
|
|
}
|
|
|
|
const deleteConvoResult = await Conversation.deleteMany(userFilter);
|
|
|
|
const deleteMessagesResult = await deleteMessages({
|
|
conversationId: { $in: conversationIds },
|
|
});
|
|
|
|
return { ...deleteConvoResult, messages: deleteMessagesResult };
|
|
} catch (error) {
|
|
logger.error('[deleteConvos] Error deleting conversations and messages', error);
|
|
throw error;
|
|
}
|
|
},
|
|
};
|