| const { logger } = require('@librechat/data-schemas'); |
| const { createTempChatExpirationDate } = require('@librechat/api'); |
| const { getMessages, deleteMessages } = require('./Message'); |
| const { Conversation } = require('~/db/models'); |
|
|
| |
| |
| |
| |
| |
| 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'); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| 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'); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| 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 = { |
| getConvoFiles, |
| searchConversation, |
| deleteNullOrEmptyConversations, |
| |
| |
| |
| |
| |
| |
| |
| 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) { |
| try { |
| const appConfig = req.config; |
| update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig); |
| } catch (err) { |
| logger.error('Error creating temporary chat expiration date:', err); |
| logger.info(`---\`saveConvo\` context: ${metadata?.context}`); |
| update.expiredAt = null; |
| } |
| } else { |
| update.expiredAt = null; |
| } |
|
|
| |
| const updateOperation = { $set: update }; |
| if (metadata && metadata.unsetFields && Object.keys(metadata.unsetFields).length > 0) { |
| updateOperation.$unset = metadata.unsetFields; |
| } |
|
|
| |
| 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, { filter: `user = "${user}"` }); |
| 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 model agent_id assistant_id spec iconURL', |
| ) |
| .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, |
| |
| getConvoTitle: async (user, conversationId) => { |
| try { |
| const convo = await getConvo(user, conversationId); |
| |
| if (convo && !convo.title) { |
| return null; |
| } else { |
| |
| return convo?.title || 'New Chat'; |
| } |
| } catch (error) { |
| logger.error('[getConvoTitle] Error getting conversation title', error); |
| return { message: 'Error getting conversation title' }; |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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; |
| } |
| }, |
| }; |
|
|