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