mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 08:20:14 +01:00
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* refactor: Token Limit Processing with Enhanced Efficiency - Added a new test suite for `processTextWithTokenLimit`, ensuring comprehensive coverage of various scenarios including under, at, and exceeding token limits. - Refactored the `processTextWithTokenLimit` function to utilize a ratio-based estimation method, significantly reducing the number of token counting function calls compared to the previous binary search approach. - Improved handling of edge cases and variable token density, ensuring accurate truncation and performance across diverse text inputs. - Included direct comparisons with the old implementation to validate correctness and efficiency improvements. * refactor: Remove Tokenizer Route and Related References - Deleted the tokenizer route from the server and removed its references from the routes index and server files, streamlining the API structure. - This change simplifies the routing configuration by eliminating unused endpoints. * refactor: Migrate countTokens Utility to API Module - Removed the local countTokens utility and integrated it into the @librechat/api module for centralized access. - Updated various files to reference the new countTokens import from the API module, ensuring consistent usage across the application. - Cleaned up unused references and imports related to the previous countTokens implementation. * refactor: Centralize escapeRegExp Utility in API Module - Moved the escapeRegExp function from local utility files to the @librechat/api module for consistent usage across the application. - Updated imports in various files to reference the new centralized escapeRegExp function, ensuring cleaner code and reducing redundancy. - Removed duplicate implementations of escapeRegExp from multiple files, streamlining the codebase. * refactor: Enhance Token Counting Flexibility in Text Processing - Updated the `processTextWithTokenLimit` function to accept both synchronous and asynchronous token counting functions, improving its versatility. - Introduced a new `TokenCountFn` type to define the token counting function signature. - Added comprehensive tests to validate the behavior of `processTextWithTokenLimit` with both sync and async token counting functions, ensuring consistent results. - Implemented a wrapper to track call counts for the `countTokens` function, optimizing performance and reducing unnecessary calls. - Enhanced existing tests to compare the performance of the new implementation against the old one, demonstrating significant improvements in efficiency. * chore: documentation for Truncation Safety Buffer in Token Processing - Added a safety buffer multiplier to the character position estimates during text truncation to prevent overshooting token limits. - Updated the `processTextWithTokenLimit` function to utilize the new `TRUNCATION_SAFETY_BUFFER` constant, enhancing the accuracy of token limit processing. - Improved documentation to clarify the rationale behind the buffer and its impact on performance and efficiency in token counting.
329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
const express = require('express');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { ContentTypes } = require('librechat-data-provider');
|
|
const { unescapeLaTeX, countTokens } = require('@librechat/api');
|
|
const {
|
|
saveConvo,
|
|
getMessage,
|
|
saveMessage,
|
|
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 { 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, { filter: `user = "${user}"` }, true);
|
|
|
|
const messages = searchResults.hits || [];
|
|
|
|
const result = await getConvosQueried(req.user.id, messages, cursor);
|
|
|
|
const messageIds = [];
|
|
const cleanedMessages = [];
|
|
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]) {
|
|
messageIds.push(message.messageId);
|
|
cleanedMessages.push(message);
|
|
}
|
|
}
|
|
|
|
const dbMessages = await getMessages({
|
|
user,
|
|
messageId: { $in: messageIds },
|
|
});
|
|
|
|
const dbMessageMap = {};
|
|
for (const dbMessage of dbMessages) {
|
|
dbMessageMap[dbMessage.messageId] = dbMessage;
|
|
}
|
|
|
|
const activeMessages = [];
|
|
for (const message of cleanedMessages) {
|
|
const convo = result.convoMap[message.conversationId];
|
|
const dbMessage = dbMessageMap[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' });
|
|
}
|
|
|
|
// Unescape LaTeX preprocessing done by the frontend
|
|
// The frontend escapes $ signs for display, but the database has unescaped versions
|
|
const unescapedOriginal = unescapeLaTeX(original);
|
|
const unescapedUpdated = unescapeLaTeX(updated);
|
|
|
|
const targetArtifact = artifacts[index];
|
|
let updatedText = null;
|
|
|
|
if (targetArtifact.source === 'content') {
|
|
const part = message.content[targetArtifact.partIndex];
|
|
updatedText = replaceArtifactContent(
|
|
part.text,
|
|
targetArtifact,
|
|
unescapedOriginal,
|
|
unescapedUpdated,
|
|
);
|
|
if (updatedText) {
|
|
part.text = updatedText;
|
|
}
|
|
} else {
|
|
updatedText = replaceArtifactContent(
|
|
message.text,
|
|
targetArtifact,
|
|
unescapedOriginal,
|
|
unescapedUpdated,
|
|
);
|
|
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' });
|
|
}
|
|
|
|
const currentPartType = updatedContent[index].type;
|
|
if (currentPartType !== ContentTypes.TEXT && currentPartType !== ContentTypes.THINK) {
|
|
return res.status(400).json({ error: 'Cannot update non-text content' });
|
|
}
|
|
|
|
const oldText = updatedContent[index][currentPartType];
|
|
updatedContent[index] = { type: currentPartType, [currentPartType]: 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;
|