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

* feat: Add BedrockIcon component to SVG library * feat: EModelEndpoint.bedrock * feat: first pass, bedrock chat. note: AgentClient is returning `agents` as conversation.endpoint * fix: declare endpoint in initialization step * chore: Update @librechat/agents dependency to version 1.4.5 * feat: backend content aggregation for agents/bedrock * feat: abort agent requests * feat: AWS Bedrock icons * WIP: agent provider schema parsing * chore: Update EditIcon props type * refactor(useGenerationsByLatest): make agents and bedrock editable * refactor: non-assistant message content, parts * fix: Bedrock response `sender` * fix: use endpointOption.model_parameters not endpointOption.modelOptions * fix: types for step handler * refactor: Update Agents.ToolCallDelta type * refactor: Remove unnecessary assignment of parentMessageId in AskController * refactor: remove unnecessary assignment of parentMessageId (agent request handler) * fix(bedrock/agents): message regeneration * refactor: dynamic form elements using react-hook-form Controllers * fix: agent icons/labels for messages * fix: agent actions * fix: use of new dynamic tags causing application crash * refactor: dynamic settings touch-ups * refactor: update Slider component to allow custom track class name * refactor: update DynamicSlider component styles * refactor: use Constants value for GLOBAL_PROJECT_NAME (enum) * feat: agent share global methods/controllers * fix: agents query * fix: `getResponseModel` * fix: share prompt a11y issue * refactor: update SharePrompt dialog theme styles * refactor: explicit typing for SharePrompt * feat: add agent roles/permissions * chore: update @librechat/agents dependency to version 1.4.7 for tool_call_ids edge case * fix(Anthropic): messages.X.content.Y.tool_use.input: Input should be a valid dictionary * fix: handle text parts with tool_call_ids and empty text * fix: role initialization * refactor: don't make instructions required * refactor: improve typing of Text part * fix: setShowStopButton for agents route * chore: remove params for now * fix: add streamBuffer and streamRate to help prevent 'Overloaded' errors from Anthropic API * refactor: remove console.log statement in ContentRender component * chore: typing, rename Context to Delete Button * chore(DeleteButton): logging * refactor(Action): make accessible * style(Action): improve a11y again * refactor: remove use/mention of mongoose sessions * feat: first pass, sharing agents * feat: visual indicator for global agent, remove author when serving to non-author * wip: params * chore: fix typing issues * fix(schemas): typing * refactor: improve accessibility of ListCard component and fix console React warning * wip: reset templates for non-legacy new convos * Revert "wip: params" This reverts commitf8067e91d4
. * Revert "refactor: dynamic form elements using react-hook-form Controllers" This reverts commit2150c4815d
. * fix(Parameters): types and parameter effect update to only update local state to parameters * refactor: optimize useDebouncedInput hook for better performance * feat: first pass, anthropic bedrock params * chore: paramEndpoints check for endpointType too * fix: maxTokens to use coerceNumber.optional(), * feat: extra chat model params * chore: reduce code repetition * refactor: improve preset title handling in SaveAsPresetDialog component * refactor: improve preset handling in HeaderOptions component * chore: improve typing, replace legacy dialog for SaveAsPresetDialog * feat: save as preset from parameters panel * fix: multi-search in select dropdown when using Option type * refactor: update default showDefault value to false in Dynamic components * feat: Bedrock presets settings * chore: config, fix agents schema, update config version * refactor: update AWS region variable name in bedrock options endpoint to BEDROCK_AWS_DEFAULT_REGION * refactor: update baseEndpointSchema in config.ts to include baseURL property * refactor: update createRun function to include req parameter and set streamRate based on provider * feat: availableRegions via config * refactor: remove unused demo agent controller file * WIP: title * Update @librechat/agents to version 1.5.0 * chore: addTitle.js to handle empty responseText * feat: support images and titles * feat: context token updates * Refactor BaseClient test to use expect.objectContaining * refactor: add model select, remove header options params, move side panel params below prompts * chore: update models list, catch title error * feat: model service for bedrock models (env) * chore: Remove verbose debug log in AgentClient class following stream * feat(bedrock): track token spend; fix: token rates, value key mapping for AWS models * refactor: handle streamRate in `handleLLMNewToken` callback * chore: AWS Bedrock example config in `.env.example` * refactor: Rename bedrockMeta to bedrockGeneral in settings.ts and use for AI21 and Amazon Bedrock providers * refactor: Update `.env.example` with AWS Bedrock model IDs URL and additional notes * feat: titleModel support for bedrock * refactor: Update `.env.example` with additional notes for AWS Bedrock model IDs
528 lines
16 KiB
JavaScript
528 lines
16 KiB
JavaScript
const { ObjectId } = require('mongodb');
|
|
const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider');
|
|
const {
|
|
getProjectByName,
|
|
addGroupIdsToProject,
|
|
removeGroupIdsFromProject,
|
|
removeGroupFromAllProjects,
|
|
} = require('./Project');
|
|
const { Prompt, PromptGroup } = require('./schema/promptSchema');
|
|
const { logger } = require('~/config');
|
|
|
|
/**
|
|
* Create a pipeline for the aggregation to get prompt groups
|
|
* @param {Object} query
|
|
* @param {number} skip
|
|
* @param {number} limit
|
|
* @returns {[Object]} - The pipeline for the aggregation
|
|
*/
|
|
const createGroupPipeline = (query, skip, limit) => {
|
|
return [
|
|
{ $match: query },
|
|
{ $sort: { createdAt: -1 } },
|
|
{ $skip: skip },
|
|
{ $limit: limit },
|
|
{
|
|
$lookup: {
|
|
from: 'prompts',
|
|
localField: 'productionId',
|
|
foreignField: '_id',
|
|
as: 'productionPrompt',
|
|
},
|
|
},
|
|
{ $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } },
|
|
{
|
|
$project: {
|
|
name: 1,
|
|
numberOfGenerations: 1,
|
|
oneliner: 1,
|
|
category: 1,
|
|
projectIds: 1,
|
|
productionId: 1,
|
|
author: 1,
|
|
authorName: 1,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
'productionPrompt.prompt': 1,
|
|
// 'productionPrompt._id': 1,
|
|
// 'productionPrompt.type': 1,
|
|
},
|
|
},
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Create a pipeline for the aggregation to get all prompt groups
|
|
* @param {Object} query
|
|
* @param {Partial<MongoPromptGroup>} $project
|
|
* @returns {[Object]} - The pipeline for the aggregation
|
|
*/
|
|
const createAllGroupsPipeline = (
|
|
query,
|
|
$project = {
|
|
name: 1,
|
|
oneliner: 1,
|
|
category: 1,
|
|
author: 1,
|
|
authorName: 1,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
command: 1,
|
|
'productionPrompt.prompt': 1,
|
|
},
|
|
) => {
|
|
return [
|
|
{ $match: query },
|
|
{ $sort: { createdAt: -1 } },
|
|
{
|
|
$lookup: {
|
|
from: 'prompts',
|
|
localField: 'productionId',
|
|
foreignField: '_id',
|
|
as: 'productionPrompt',
|
|
},
|
|
},
|
|
{ $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } },
|
|
{
|
|
$project,
|
|
},
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Get all prompt groups with filters
|
|
* @param {Object} req
|
|
* @param {TPromptGroupsWithFilterRequest} filter
|
|
* @returns {Promise<PromptGroupListResponse>}
|
|
*/
|
|
const getAllPromptGroups = async (req, filter) => {
|
|
try {
|
|
const { name, ...query } = filter;
|
|
|
|
if (!query.author) {
|
|
throw new Error('Author is required');
|
|
}
|
|
|
|
let searchShared = true;
|
|
let searchSharedOnly = false;
|
|
if (name) {
|
|
query.name = new RegExp(name, 'i');
|
|
}
|
|
if (!query.category) {
|
|
delete query.category;
|
|
} else if (query.category === SystemCategories.MY_PROMPTS) {
|
|
searchShared = false;
|
|
delete query.category;
|
|
} else if (query.category === SystemCategories.NO_CATEGORY) {
|
|
query.category = '';
|
|
} else if (query.category === SystemCategories.SHARED_PROMPTS) {
|
|
searchSharedOnly = true;
|
|
delete query.category;
|
|
}
|
|
|
|
let combinedQuery = query;
|
|
|
|
if (searchShared) {
|
|
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
|
|
if (project && project.promptGroupIds.length > 0) {
|
|
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
|
|
delete projectQuery.author;
|
|
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
|
|
}
|
|
}
|
|
|
|
const promptGroupsPipeline = createAllGroupsPipeline(combinedQuery);
|
|
return await PromptGroup.aggregate(promptGroupsPipeline).exec();
|
|
} catch (error) {
|
|
console.error('Error getting all prompt groups', error);
|
|
return { message: 'Error getting all prompt groups' };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get prompt groups with filters
|
|
* @param {Object} req
|
|
* @param {TPromptGroupsWithFilterRequest} filter
|
|
* @returns {Promise<PromptGroupListResponse>}
|
|
*/
|
|
const getPromptGroups = async (req, filter) => {
|
|
try {
|
|
const { pageNumber = 1, pageSize = 10, name, ...query } = filter;
|
|
|
|
const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1);
|
|
const validatedPageSize = Math.max(parseInt(pageSize, 10), 1);
|
|
|
|
if (!query.author) {
|
|
throw new Error('Author is required');
|
|
}
|
|
|
|
let searchShared = true;
|
|
let searchSharedOnly = false;
|
|
if (name) {
|
|
query.name = new RegExp(name, 'i');
|
|
}
|
|
if (!query.category) {
|
|
delete query.category;
|
|
} else if (query.category === SystemCategories.MY_PROMPTS) {
|
|
searchShared = false;
|
|
delete query.category;
|
|
} else if (query.category === SystemCategories.NO_CATEGORY) {
|
|
query.category = '';
|
|
} else if (query.category === SystemCategories.SHARED_PROMPTS) {
|
|
searchSharedOnly = true;
|
|
delete query.category;
|
|
}
|
|
|
|
let combinedQuery = query;
|
|
|
|
if (searchShared) {
|
|
// const projects = req.user.projects || []; // TODO: handle multiple projects
|
|
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
|
|
if (project && project.promptGroupIds.length > 0) {
|
|
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
|
|
delete projectQuery.author;
|
|
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
|
|
}
|
|
}
|
|
|
|
const skip = (validatedPageNumber - 1) * validatedPageSize;
|
|
const limit = validatedPageSize;
|
|
|
|
const promptGroupsPipeline = createGroupPipeline(combinedQuery, skip, limit);
|
|
const totalPromptGroupsPipeline = [{ $match: combinedQuery }, { $count: 'total' }];
|
|
|
|
const [promptGroupsResults, totalPromptGroupsResults] = await Promise.all([
|
|
PromptGroup.aggregate(promptGroupsPipeline).exec(),
|
|
PromptGroup.aggregate(totalPromptGroupsPipeline).exec(),
|
|
]);
|
|
|
|
const promptGroups = promptGroupsResults;
|
|
const totalPromptGroups =
|
|
totalPromptGroupsResults.length > 0 ? totalPromptGroupsResults[0].total : 0;
|
|
|
|
return {
|
|
promptGroups,
|
|
pageNumber: validatedPageNumber.toString(),
|
|
pageSize: validatedPageSize.toString(),
|
|
pages: Math.ceil(totalPromptGroups / validatedPageSize).toString(),
|
|
};
|
|
} catch (error) {
|
|
console.error('Error getting prompt groups', error);
|
|
return { message: 'Error getting prompt groups' };
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getPromptGroups,
|
|
getAllPromptGroups,
|
|
/**
|
|
* Create a prompt and its respective group
|
|
* @param {TCreatePromptRecord} saveData
|
|
* @returns {Promise<TCreatePromptResponse>}
|
|
*/
|
|
createPromptGroup: async (saveData) => {
|
|
try {
|
|
const { prompt, group, author, authorName } = saveData;
|
|
|
|
let newPromptGroup = await PromptGroup.findOneAndUpdate(
|
|
{ ...group, author, authorName, productionId: null },
|
|
{ $setOnInsert: { ...group, author, authorName, productionId: null } },
|
|
{ new: true, upsert: true },
|
|
)
|
|
.lean()
|
|
.select('-__v')
|
|
.exec();
|
|
|
|
const newPrompt = await Prompt.findOneAndUpdate(
|
|
{ ...prompt, author, groupId: newPromptGroup._id },
|
|
{ $setOnInsert: { ...prompt, author, groupId: newPromptGroup._id } },
|
|
{ new: true, upsert: true },
|
|
)
|
|
.lean()
|
|
.select('-__v')
|
|
.exec();
|
|
|
|
newPromptGroup = await PromptGroup.findByIdAndUpdate(
|
|
newPromptGroup._id,
|
|
{ productionId: newPrompt._id },
|
|
{ new: true },
|
|
)
|
|
.lean()
|
|
.select('-__v')
|
|
.exec();
|
|
|
|
return {
|
|
prompt: newPrompt,
|
|
group: {
|
|
...newPromptGroup,
|
|
productionPrompt: { prompt: newPrompt.prompt },
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error saving prompt group', error);
|
|
throw new Error('Error saving prompt group');
|
|
}
|
|
},
|
|
/**
|
|
* Save a prompt
|
|
* @param {TCreatePromptRecord} saveData
|
|
* @returns {Promise<TCreatePromptResponse>}
|
|
*/
|
|
savePrompt: async (saveData) => {
|
|
try {
|
|
const { prompt, author } = saveData;
|
|
const newPromptData = {
|
|
...prompt,
|
|
author,
|
|
};
|
|
|
|
/** @type {TPrompt} */
|
|
let newPrompt;
|
|
try {
|
|
newPrompt = await Prompt.create(newPromptData);
|
|
} catch (error) {
|
|
if (error?.message?.includes('groupId_1_version_1')) {
|
|
await Prompt.db.collection('prompts').dropIndex('groupId_1_version_1');
|
|
} else {
|
|
throw error;
|
|
}
|
|
newPrompt = await Prompt.create(newPromptData);
|
|
}
|
|
|
|
return { prompt: newPrompt };
|
|
} catch (error) {
|
|
logger.error('Error saving prompt', error);
|
|
return { message: 'Error saving prompt' };
|
|
}
|
|
},
|
|
getPrompts: async (filter) => {
|
|
try {
|
|
return await Prompt.find(filter).sort({ createdAt: -1 }).lean();
|
|
} catch (error) {
|
|
logger.error('Error getting prompts', error);
|
|
return { message: 'Error getting prompts' };
|
|
}
|
|
},
|
|
getPrompt: async (filter) => {
|
|
try {
|
|
if (filter.groupId) {
|
|
filter.groupId = new ObjectId(filter.groupId);
|
|
}
|
|
return await Prompt.findOne(filter).lean();
|
|
} catch (error) {
|
|
logger.error('Error getting prompt', error);
|
|
return { message: 'Error getting prompt' };
|
|
}
|
|
},
|
|
/**
|
|
* Get prompt groups with filters
|
|
* @param {TGetRandomPromptsRequest} filter
|
|
* @returns {Promise<TGetRandomPromptsResponse>}
|
|
*/
|
|
getRandomPromptGroups: async (filter) => {
|
|
try {
|
|
const result = await PromptGroup.aggregate([
|
|
{
|
|
$match: {
|
|
category: { $ne: '' },
|
|
},
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$category',
|
|
promptGroup: { $first: '$$ROOT' },
|
|
},
|
|
},
|
|
{
|
|
$replaceRoot: { newRoot: '$promptGroup' },
|
|
},
|
|
{
|
|
$sample: { size: +filter.limit + +filter.skip },
|
|
},
|
|
{
|
|
$skip: +filter.skip,
|
|
},
|
|
{
|
|
$limit: +filter.limit,
|
|
},
|
|
]);
|
|
return { prompts: result };
|
|
} catch (error) {
|
|
logger.error('Error getting prompt groups', error);
|
|
return { message: 'Error getting prompt groups' };
|
|
}
|
|
},
|
|
getPromptGroupsWithPrompts: async (filter) => {
|
|
try {
|
|
return await PromptGroup.findOne(filter)
|
|
.populate({
|
|
path: 'prompts',
|
|
select: '-_id -__v -user',
|
|
})
|
|
.select('-_id -__v -user')
|
|
.lean();
|
|
} catch (error) {
|
|
logger.error('Error getting prompt groups', error);
|
|
return { message: 'Error getting prompt groups' };
|
|
}
|
|
},
|
|
getPromptGroup: async (filter) => {
|
|
try {
|
|
return await PromptGroup.findOne(filter).lean();
|
|
} catch (error) {
|
|
logger.error('Error getting prompt group', error);
|
|
return { message: 'Error getting prompt group' };
|
|
}
|
|
},
|
|
/**
|
|
* Deletes a prompt and its corresponding prompt group if it is the last prompt in the group.
|
|
*
|
|
* @param {Object} options - The options for deleting the prompt.
|
|
* @param {ObjectId|string} options.promptId - The ID of the prompt to delete.
|
|
* @param {ObjectId|string} options.groupId - The ID of the prompt's group.
|
|
* @param {ObjectId|string} options.author - The ID of the prompt's author.
|
|
* @param {string} options.role - The role of the prompt's author.
|
|
* @return {Promise<TDeletePromptResponse>} An object containing the result of the deletion.
|
|
* If the prompt was deleted successfully, the object will have a property 'prompt' with the value 'Prompt deleted successfully'.
|
|
* If the prompt group was deleted successfully, the object will have a property 'promptGroup' with the message 'Prompt group deleted successfully' and id of the deleted group.
|
|
* If there was an error deleting the prompt, the object will have a property 'message' with the value 'Error deleting prompt'.
|
|
*/
|
|
deletePrompt: async ({ promptId, groupId, author, role }) => {
|
|
const query = { _id: promptId, groupId, author };
|
|
if (role === SystemRoles.ADMIN) {
|
|
delete query.author;
|
|
}
|
|
const { deletedCount } = await Prompt.deleteOne(query);
|
|
if (deletedCount === 0) {
|
|
throw new Error('Failed to delete the prompt');
|
|
}
|
|
|
|
const remainingPrompts = await Prompt.find({ groupId })
|
|
.select('_id')
|
|
.sort({ createdAt: 1 })
|
|
.lean();
|
|
|
|
if (remainingPrompts.length === 0) {
|
|
await PromptGroup.deleteOne({ _id: groupId });
|
|
await removeGroupFromAllProjects(groupId);
|
|
|
|
return {
|
|
prompt: 'Prompt deleted successfully',
|
|
promptGroup: {
|
|
message: 'Prompt group deleted successfully',
|
|
id: groupId,
|
|
},
|
|
};
|
|
} else {
|
|
const promptGroup = await PromptGroup.findById(groupId).lean();
|
|
if (promptGroup.productionId.toString() === promptId.toString()) {
|
|
await PromptGroup.updateOne(
|
|
{ _id: groupId },
|
|
{ productionId: remainingPrompts[remainingPrompts.length - 1]._id },
|
|
);
|
|
}
|
|
|
|
return { prompt: 'Prompt deleted successfully' };
|
|
}
|
|
},
|
|
/**
|
|
* Update prompt group
|
|
* @param {Partial<MongoPromptGroup>} filter - Filter to find prompt group
|
|
* @param {Partial<MongoPromptGroup>} data - Data to update
|
|
* @returns {Promise<TUpdatePromptGroupResponse>}
|
|
*/
|
|
updatePromptGroup: async (filter, data) => {
|
|
try {
|
|
const updateOps = {};
|
|
if (data.removeProjectIds) {
|
|
for (const projectId of data.removeProjectIds) {
|
|
await removeGroupIdsFromProject(projectId, [filter._id]);
|
|
}
|
|
|
|
updateOps.$pull = { projectIds: { $in: data.removeProjectIds } };
|
|
delete data.removeProjectIds;
|
|
}
|
|
|
|
if (data.projectIds) {
|
|
for (const projectId of data.projectIds) {
|
|
await addGroupIdsToProject(projectId, [filter._id]);
|
|
}
|
|
|
|
updateOps.$addToSet = { projectIds: { $each: data.projectIds } };
|
|
delete data.projectIds;
|
|
}
|
|
|
|
const updateData = { ...data, ...updateOps };
|
|
const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, {
|
|
new: true,
|
|
upsert: false,
|
|
});
|
|
|
|
if (!updatedDoc) {
|
|
throw new Error('Prompt group not found');
|
|
}
|
|
|
|
return updatedDoc;
|
|
} catch (error) {
|
|
logger.error('Error updating prompt group', error);
|
|
return { message: 'Error updating prompt group' };
|
|
}
|
|
},
|
|
/**
|
|
* Function to make a prompt production based on its ID.
|
|
* @param {String} promptId - The ID of the prompt to make production.
|
|
* @returns {Object} The result of the production operation.
|
|
*/
|
|
makePromptProduction: async (promptId) => {
|
|
try {
|
|
const prompt = await Prompt.findById(promptId).lean();
|
|
|
|
if (!prompt) {
|
|
throw new Error('Prompt not found');
|
|
}
|
|
|
|
await PromptGroup.findByIdAndUpdate(
|
|
prompt.groupId,
|
|
{ productionId: prompt._id },
|
|
{ new: true },
|
|
)
|
|
.lean()
|
|
.exec();
|
|
|
|
return {
|
|
message: 'Prompt production made successfully',
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error making prompt production', error);
|
|
return { message: 'Error making prompt production' };
|
|
}
|
|
},
|
|
updatePromptLabels: async (_id, labels) => {
|
|
try {
|
|
const response = await Prompt.updateOne({ _id }, { $set: { labels } });
|
|
if (response.matchedCount === 0) {
|
|
return { message: 'Prompt not found' };
|
|
}
|
|
return { message: 'Prompt labels updated successfully' };
|
|
} catch (error) {
|
|
logger.error('Error updating prompt labels', error);
|
|
return { message: 'Error updating prompt labels' };
|
|
}
|
|
},
|
|
deletePromptGroup: async (_id) => {
|
|
try {
|
|
const response = await PromptGroup.deleteOne({ _id });
|
|
|
|
if (response.deletedCount === 0) {
|
|
return { promptGroup: 'Prompt group not found' };
|
|
}
|
|
|
|
await Prompt.deleteMany({ groupId: new ObjectId(_id) });
|
|
await removeGroupFromAllProjects(_id);
|
|
return { promptGroup: 'Prompt group deleted successfully' };
|
|
} catch (error) {
|
|
logger.error('Error deleting prompt group', error);
|
|
return { message: 'Error deleting prompt group' };
|
|
}
|
|
},
|
|
};
|