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

* refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports
296 lines
9.5 KiB
JavaScript
296 lines
9.5 KiB
JavaScript
const express = require('express');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { ContentTypes } = require('librechat-data-provider');
|
|
const {
|
|
saveConvo,
|
|
saveMessage,
|
|
getMessage,
|
|
getMessages,
|
|
updateMessage,
|
|
deleteMessages,
|
|
} = require('~/models');
|
|
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
|
|
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
|
|
const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
|
|
const { getConvosQueried } = require('~/models/Conversation');
|
|
const { countTokens } = require('~/server/utils');
|
|
const { Message } = require('~/db/models');
|
|
|
|
const router = express.Router();
|
|
router.use(requireJwtAuth);
|
|
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const user = req.user.id ?? '';
|
|
const {
|
|
cursor = null,
|
|
sortBy = 'createdAt',
|
|
sortDirection = 'desc',
|
|
pageSize: pageSizeRaw,
|
|
conversationId,
|
|
messageId,
|
|
search,
|
|
} = req.query;
|
|
const pageSize = parseInt(pageSizeRaw, 10) || 25;
|
|
|
|
let response;
|
|
const sortField = ['endpoint', 'createdAt', 'updatedAt'].includes(sortBy)
|
|
? sortBy
|
|
: 'createdAt';
|
|
const sortOrder = sortDirection === 'asc' ? 1 : -1;
|
|
|
|
if (conversationId && messageId) {
|
|
const message = await Message.findOne({
|
|
conversationId,
|
|
messageId,
|
|
user: user,
|
|
}).lean();
|
|
response = { messages: message ? [message] : [], nextCursor: null };
|
|
} else if (conversationId) {
|
|
const filter = { conversationId, user: user };
|
|
if (cursor) {
|
|
filter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor };
|
|
}
|
|
const messages = await Message.find(filter)
|
|
.sort({ [sortField]: sortOrder })
|
|
.limit(pageSize + 1)
|
|
.lean();
|
|
const nextCursor = messages.length > pageSize ? messages.pop()[sortField] : null;
|
|
response = { messages, nextCursor };
|
|
} else if (search) {
|
|
const searchResults = await Message.meiliSearch(search, undefined, true);
|
|
|
|
const messages = searchResults.hits || [];
|
|
|
|
const result = await getConvosQueried(req.user.id, messages, cursor);
|
|
|
|
const activeMessages = [];
|
|
for (let i = 0; i < messages.length; i++) {
|
|
let message = messages[i];
|
|
if (message.conversationId.includes('--')) {
|
|
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
|
|
}
|
|
if (result.convoMap[message.conversationId]) {
|
|
const convo = result.convoMap[message.conversationId];
|
|
|
|
const dbMessage = await getMessage({ user, messageId: message.messageId });
|
|
activeMessages.push({
|
|
...message,
|
|
title: convo.title,
|
|
conversationId: message.conversationId,
|
|
model: convo.model,
|
|
isCreatedByUser: dbMessage?.isCreatedByUser,
|
|
endpoint: dbMessage?.endpoint,
|
|
iconURL: dbMessage?.iconURL,
|
|
});
|
|
}
|
|
}
|
|
|
|
response = { messages: activeMessages, nextCursor: null };
|
|
} else {
|
|
response = { messages: [], nextCursor: null };
|
|
}
|
|
|
|
res.status(200).json(response);
|
|
} catch (error) {
|
|
logger.error('Error fetching messages:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/artifact/:messageId', async (req, res) => {
|
|
try {
|
|
const { messageId } = req.params;
|
|
const { index, original, updated } = req.body;
|
|
|
|
if (typeof index !== 'number' || index < 0 || original == null || updated == null) {
|
|
return res.status(400).json({ error: 'Invalid request parameters' });
|
|
}
|
|
|
|
const message = await getMessage({ user: req.user.id, messageId });
|
|
if (!message) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
|
|
const artifacts = findAllArtifacts(message);
|
|
if (index >= artifacts.length) {
|
|
return res.status(400).json({ error: 'Artifact index out of bounds' });
|
|
}
|
|
|
|
const targetArtifact = artifacts[index];
|
|
let updatedText = null;
|
|
|
|
if (targetArtifact.source === 'content') {
|
|
const part = message.content[targetArtifact.partIndex];
|
|
updatedText = replaceArtifactContent(part.text, targetArtifact, original, updated);
|
|
if (updatedText) {
|
|
part.text = updatedText;
|
|
}
|
|
} else {
|
|
updatedText = replaceArtifactContent(message.text, targetArtifact, original, updated);
|
|
if (updatedText) {
|
|
message.text = updatedText;
|
|
}
|
|
}
|
|
|
|
if (!updatedText) {
|
|
return res.status(400).json({ error: 'Original content not found in target artifact' });
|
|
}
|
|
|
|
const savedMessage = await saveMessage(
|
|
req,
|
|
{
|
|
messageId,
|
|
conversationId: message.conversationId,
|
|
text: message.text,
|
|
content: message.content,
|
|
user: req.user.id,
|
|
},
|
|
{ context: 'POST /api/messages/artifact/:messageId' },
|
|
);
|
|
|
|
res.status(200).json({
|
|
conversationId: savedMessage.conversationId,
|
|
content: savedMessage.content,
|
|
text: savedMessage.text,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error editing artifact:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/* Note: It's necessary to add `validateMessageReq` within route definition for correct params */
|
|
router.get('/:conversationId', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const { conversationId } = req.params;
|
|
const messages = await getMessages({ conversationId }, '-_id -__v -user');
|
|
res.status(200).json(messages);
|
|
} catch (error) {
|
|
logger.error('Error fetching messages:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/:conversationId', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const message = req.body;
|
|
const savedMessage = await saveMessage(
|
|
req,
|
|
{ ...message, user: req.user.id },
|
|
{ context: 'POST /api/messages/:conversationId' },
|
|
);
|
|
if (!savedMessage) {
|
|
return res.status(400).json({ error: 'Message not saved' });
|
|
}
|
|
await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' });
|
|
res.status(201).json(savedMessage);
|
|
} catch (error) {
|
|
logger.error('Error saving message:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const { conversationId, messageId } = req.params;
|
|
const message = await getMessages({ conversationId, messageId }, '-_id -__v -user');
|
|
if (!message) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
res.status(200).json(message);
|
|
} catch (error) {
|
|
logger.error('Error fetching message:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const { conversationId, messageId } = req.params;
|
|
const { text, index, model } = req.body;
|
|
|
|
if (index === undefined) {
|
|
const tokenCount = await countTokens(text, model);
|
|
const result = await updateMessage(req, { messageId, text, tokenCount });
|
|
return res.status(200).json(result);
|
|
}
|
|
|
|
if (typeof index !== 'number' || index < 0) {
|
|
return res.status(400).json({ error: 'Invalid index' });
|
|
}
|
|
|
|
const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0];
|
|
if (!message) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
|
|
const existingContent = message.content;
|
|
if (!Array.isArray(existingContent) || index >= existingContent.length) {
|
|
return res.status(400).json({ error: 'Invalid index' });
|
|
}
|
|
|
|
const updatedContent = [...existingContent];
|
|
if (!updatedContent[index]) {
|
|
return res.status(400).json({ error: 'Content part not found' });
|
|
}
|
|
|
|
if (updatedContent[index].type !== ContentTypes.TEXT) {
|
|
return res.status(400).json({ error: 'Cannot update non-text content' });
|
|
}
|
|
|
|
const oldText = updatedContent[index].text;
|
|
updatedContent[index] = { type: ContentTypes.TEXT, text };
|
|
|
|
let tokenCount = message.tokenCount;
|
|
if (tokenCount !== undefined) {
|
|
const oldTokenCount = await countTokens(oldText, model);
|
|
const newTokenCount = await countTokens(text, model);
|
|
tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount;
|
|
}
|
|
|
|
const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount });
|
|
return res.status(200).json(result);
|
|
} catch (error) {
|
|
logger.error('Error updating message:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const { conversationId, messageId } = req.params;
|
|
const { feedback } = req.body;
|
|
|
|
const updatedMessage = await updateMessage(
|
|
req,
|
|
{
|
|
messageId,
|
|
feedback: feedback || null,
|
|
},
|
|
{ context: 'updateFeedback' },
|
|
);
|
|
|
|
res.json({
|
|
messageId,
|
|
conversationId,
|
|
feedback: updatedMessage.feedback,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error updating message feedback:', error);
|
|
res.status(500).json({ error: 'Failed to update feedback' });
|
|
}
|
|
});
|
|
|
|
router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
|
|
try {
|
|
const { messageId } = req.params;
|
|
await deleteMessages({ messageId });
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
logger.error('Error deleting message:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|