LibreChat/api/server/controllers/agents/v1.js
Danny Avila d6a17784dc
🔗 feat: Agent Chain (Mixture-of-Agents) (#6374)
* wip: first pass, dropdown for selecting sequential agents

* refactor: Improve agent selection logic and enhance performance in SequentialAgents component

* wip: seq. agents working ideas

* wip: sequential agents style change

* refactor: move agent form options/submission outside of AgentConfig

* refactor: prevent repeating code

* refactor: simplify current agent display in SequentialAgents component

* feat: persist  form value handling in AgentSelect component for agent_ids

* feat: first pass, sequential agnets agent update

* feat: enhance message display with agent updates and empty text handling

* chore: update Icon component to use EModelEndpoint for agent endpoints

* feat: update content type checks in BaseClient to use constants for better readability

* feat: adjust max context tokens calculation to use 90% of the model's max tokens

* feat: first pass, agent run message pruning

* chore: increase max listeners for abort controller to prevent memory leaks

* feat: enhance runAgent function to include current index count map for improved token tracking

* chore: update @librechat/agents dependency to version 2.2.5

* feat: update icons and style of SequentialAgents component for improved UI consistency

* feat: add AdvancedButton and AdvancedPanel components for enhanced agent settings navigation, update styling for agent form

* chore: adjust minimum height of AdvancedPanel component for better layout consistency

* chore: update @librechat/agents dependency to version 2.2.6

* feat: enhance message formatting by incorporating tool set into agent message processing, in order to allow better mix/matching of agents (as tool calls for tools not found in set will be stringified)

* refactor: reorder components in AgentConfig for improved readability and maintainability

* refactor: enhance layout of AgentUpdate component for improved visual structure

* feat: add DeepSeek provider to Bedrock settings and schemas

* feat: enhance link styling in mobile.css for better visibility and accessibility

* fix: update banner model import in update banner script; export Banner model

* refactor: `duplicateAgentHandler` to include tool_resources only for OCR context files

* feat: add 'qwen-vl' to visionModels for enhanced model support

* fix: change image format from JPEG to PNG in DALLE3 response

* feat: reorganize Advanced components and add localizations

* refactor: simplify JSX structure in AgentChain component to defer container styling to parent

* feat: add FormInput component for reusable input handling

* feat: make agent recursion limit configurable from builder

* feat: add support for agent capabilities chain in AdvancedPanel and update data-provider version

* feat: add maxRecursionLimit configuration for agents and update related documentation

* fix: update CONFIG_VERSION to 1.2.3 in data provider configuration

* feat: replace recursion limit input with MaxAgentSteps component and enhance input handling

* feat: enhance AgentChain component with hover card for additional information and update related labels

* fix: pass request and response objects to `createActionTool` when using assistant actions to prevent auth error

* feat: update AgentChain component layout to include agent count display

* feat: increase default max listeners and implement capability check function for agent chain

* fix: update link styles in mobile.css for better visibility in dark mode

* chore: temp. remove agents package while bumping shared packages

* chore: update @langchain/google-genai package to version 0.1.11

* chore: update @langchain/google-vertexai package to version 0.2.2

* chore: add @librechat/agents package at version 2.2.8

* feat: add deepseek.r1 model with token rate and context values for bedrock
2025-03-17 16:43:44 -04:00

407 lines
12 KiB
JavaScript

const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const {
Tools,
Constants,
FileContext,
SystemRoles,
EToolResources,
actionDelimiter,
} = require('librechat-data-provider');
const {
getAgent,
createAgent,
updateAgent,
deleteAgent,
getListAgents,
} = require('~/models/Agent');
const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { updateAction, getActions } = require('~/models/Action');
const { getProjectByName } = require('~/models/Project');
const { updateAgentProjects } = require('~/models/Agent');
const { deleteFileByFilter } = require('~/models/File');
const { logger } = require('~/config');
const systemTools = {
[Tools.execute_code]: true,
[Tools.file_search]: true,
};
/**
* Creates an Agent.
* @route POST /Agents
* @param {ServerRequest} req - The request object.
* @param {AgentCreateParams} req.body - The request body.
* @param {ServerResponse} res - The response object.
* @returns {Agent} 201 - success response - application/json
*/
const createAgentHandler = async (req, res) => {
try {
const { tools = [], provider, name, description, instructions, model, ...agentData } = req.body;
const { id: userId } = req.user;
agentData.tools = [];
for (const tool of tools) {
if (req.app.locals.availableTools[tool]) {
agentData.tools.push(tool);
}
if (systemTools[tool]) {
agentData.tools.push(tool);
}
}
Object.assign(agentData, {
author: userId,
name,
description,
instructions,
provider,
model,
});
agentData.id = `agent_${nanoid()}`;
const agent = await createAgent(agentData);
res.status(201).json(agent);
} catch (error) {
logger.error('[/Agents] Error creating agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Retrieves an Agent by ID.
* @route GET /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @param {object} req.user - Authenticated user information
* @param {string} req.user.id - User ID
* @returns {Promise<Agent>} 200 - success response - application/json
* @returns {Error} 404 - Agent not found
*/
const getAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const author = req.user.id;
let query = { id, author };
const globalProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, ['agentIds']);
if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) {
query = {
$or: [{ id, $in: globalProject.agentIds }, query],
};
}
const agent = await getAgent(query);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
agent.author = agent.author.toString();
agent.isCollaborative = !!agent.isCollaborative;
if (agent.author !== author) {
delete agent.author;
}
if (!agent.isCollaborative && agent.author !== author && req.user.role !== SystemRoles.ADMIN) {
return res.status(200).json({
id: agent.id,
name: agent.name,
avatar: agent.avatar,
author: agent.author,
projectIds: agent.projectIds,
isCollaborative: agent.isCollaborative,
});
}
return res.status(200).json(agent);
} catch (error) {
logger.error('[/Agents/:id] Error retrieving agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Updates an Agent.
* @route PATCH /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @param {AgentUpdateParams} req.body - The Agent update parameters.
* @returns {Agent} 200 - success response - application/json
*/
const updateAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const { projectIds, removeProjectIds, ...updateData } = req.body;
const isAdmin = req.user.role === SystemRoles.ADMIN;
const existingAgent = await getAgent({ id });
const isAuthor = existingAgent.author.toString() === req.user.id;
if (!existingAgent) {
return res.status(404).json({ error: 'Agent not found' });
}
const hasEditPermission = existingAgent.isCollaborative || isAdmin || isAuthor;
if (!hasEditPermission) {
return res.status(403).json({
error: 'You do not have permission to modify this non-collaborative agent',
});
}
let updatedAgent =
Object.keys(updateData).length > 0 ? await updateAgent({ id }, updateData) : existingAgent;
if (projectIds || removeProjectIds) {
updatedAgent = await updateAgentProjects({
user: req.user,
agentId: id,
projectIds,
removeProjectIds,
});
}
if (updatedAgent.author) {
updatedAgent.author = updatedAgent.author.toString();
}
if (updatedAgent.author !== req.user.id) {
delete updatedAgent.author;
}
return res.json(updatedAgent);
} catch (error) {
logger.error('[/Agents/:id] Error updating Agent', error);
res.status(500).json({ error: error.message });
}
};
/**
* Duplicates an Agent based on the provided ID.
* @route POST /Agents/:id/duplicate
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 201 - success response - application/json
*/
const duplicateAgentHandler = async (req, res) => {
const { id } = req.params;
const { id: userId } = req.user;
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
try {
const agent = await getAgent({ id });
if (!agent) {
return res.status(404).json({
error: 'Agent not found',
status: 'error',
});
}
const {
id: _id,
_id: __id,
author: _author,
createdAt: _createdAt,
updatedAt: _updatedAt,
tool_resources: _tool_resources = {},
...cloneData
} = agent;
if (_tool_resources?.[EToolResources.ocr]) {
cloneData.tool_resources = {
[EToolResources.ocr]: _tool_resources[EToolResources.ocr],
};
}
const newAgentId = `agent_${nanoid()}`;
const newAgentData = Object.assign(cloneData, {
id: newAgentId,
author: userId,
});
const newActionsList = [];
const originalActions = (await getActions({ agent_id: id }, true)) ?? [];
const promises = [];
/**
* Duplicates an action and returns the new action ID.
* @param {Action} action
* @returns {Promise<string>}
*/
const duplicateAction = async (action) => {
const newActionId = nanoid();
const [domain] = action.action_id.split(actionDelimiter);
const fullActionId = `${domain}${actionDelimiter}${newActionId}`;
const newAction = await updateAction(
{ action_id: newActionId },
{
metadata: action.metadata,
agent_id: newAgentId,
user: userId,
},
);
const filteredMetadata = { ...newAction.metadata };
for (const field of sensitiveFields) {
delete filteredMetadata[field];
}
newAction.metadata = filteredMetadata;
newActionsList.push(newAction);
return fullActionId;
};
for (const action of originalActions) {
promises.push(
duplicateAction(action).catch((error) => {
logger.error('[/agents/:id/duplicate] Error duplicating Action:', error);
}),
);
}
const agentActions = await Promise.all(promises);
newAgentData.actions = agentActions;
const newAgent = await createAgent(newAgentData);
return res.status(201).json({
agent: newAgent,
actions: newActionsList,
});
} catch (error) {
logger.error('[/Agents/:id/duplicate] Error duplicating Agent:', error);
res.status(500).json({ error: error.message });
}
};
/**
* Deletes an Agent based on the provided ID.
* @route DELETE /Agents/:id
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 200 - success response - application/json
*/
const deleteAgentHandler = async (req, res) => {
try {
const id = req.params.id;
const agent = await getAgent({ id });
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
await deleteAgent({ id, author: req.user.id });
return res.json({ message: 'Agent deleted' });
} catch (error) {
logger.error('[/Agents/:id] Error deleting Agent', error);
res.status(500).json({ error: error.message });
}
};
/**
*
* @route GET /Agents
* @param {object} req - Express Request
* @param {object} req.query - Request query
* @param {string} [req.query.user] - The user ID of the agent's author.
* @returns {Promise<AgentListResponse>} 200 - success response - application/json
*/
const getListAgentsHandler = async (req, res) => {
try {
const data = await getListAgents({
author: req.user.id,
});
return res.json(data);
} catch (error) {
logger.error('[/Agents] Error listing Agents', error);
res.status(500).json({ error: error.message });
}
};
/**
* Uploads and updates an avatar for a specific agent.
* @route POST /:agent_id/avatar
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.agent_id - The ID of the agent.
* @param {Express.Multer.File} req.file - The avatar image file.
* @param {object} req.body - Request body
* @param {string} [req.body.avatar] - Optional avatar for the agent's avatar.
* @returns {Object} 200 - success response - application/json
*/
const uploadAgentAvatarHandler = async (req, res) => {
try {
filterFile({ req, file: req.file, image: true, isAvatar: true });
const { agent_id } = req.params;
if (!agent_id) {
return res.status(400).json({ message: 'Agent ID is required' });
}
const buffer = await fs.readFile(req.file.path);
const image = await uploadImageBuffer({
req,
context: FileContext.avatar,
metadata: { buffer },
});
let _avatar;
try {
const agent = await getAgent({ id: agent_id });
_avatar = agent.avatar;
} catch (error) {
logger.error('[/:agent_id/avatar] Error fetching agent', error);
_avatar = {};
}
if (_avatar && _avatar.source) {
const { deleteFile } = getStrategyFunctions(_avatar.source);
try {
await deleteFile(req, { filepath: _avatar.filepath });
await deleteFileByFilter({ user: req.user.id, filepath: _avatar.filepath });
} catch (error) {
logger.error('[/:agent_id/avatar] Error deleting old avatar', error);
}
}
const promises = [];
const data = {
avatar: {
filepath: image.filepath,
source: req.app.locals.fileStrategy,
},
};
promises.push(await updateAgent({ id: agent_id, author: req.user.id }, data));
const resolved = await Promise.all(promises);
res.status(201).json(resolved[0]);
} catch (error) {
const message = 'An error occurred while updating the Agent Avatar';
logger.error(message, error);
res.status(500).json({ message });
} finally {
try {
await fs.unlink(req.file.path);
logger.debug('[/:agent_id/avatar] Temp. image upload file deleted');
} catch (error) {
logger.debug('[/:agent_id/avatar] Temp. image upload file already deleted');
}
}
};
module.exports = {
createAgent: createAgentHandler,
getAgent: getAgentHandler,
updateAgent: updateAgentHandler,
duplicateAgent: duplicateAgentHandler,
deleteAgent: deleteAgentHandler,
getListAgents: getListAgentsHandler,
uploadAgentAvatar: uploadAgentAvatarHandler,
};