mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-02 16:48:50 +01:00
🫱🏼🫲🏽 refactor: Improve Agent Handoffs (#11172)
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
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
* fix: Tool Resources Dropped between Agent Handoffs * fix: agent deletion process to remove handoff edges - Added logic to the `deleteAgent` function to remove references to the deleted agent from other agents' handoff edges. - Implemented error handling to log any issues encountered during the edge removal process. - Introduced a new test case to verify that handoff edges are correctly removed when an agent is deleted, ensuring data integrity across agent relationships. * fix: Improve agent loading process by handling orphaned references - Added logic to track and log agents that fail to load during initialization, preventing errors from interrupting the process. - Introduced a Set to store skipped agent IDs and updated edge filtering to exclude these orphaned references, enhancing data integrity in agent relationships. * chore: Update @librechat/agents to version 3.0.62 * feat: Enhance agent initialization with edge collection and filtering - Introduced new functions for edge collection and filtering orphaned edges, improving the agent loading process. - Refactored the `initializeClient` function to utilize breadth-first search (BFS) for discovering connected agents, enabling transitive handoffs. - Added a new module for edge-related utilities, including deduplication and participant extraction, to streamline edge management. - Updated the agent configuration handling to ensure proper edge processing and integrity during initialization. * refactor: primary agent ID selection for multi-agent conversations - Added a new function `findPrimaryAgentId` to determine the primary agent ID from a set of agent IDs based on suffix rules. - Updated `createMultiAgentMapper` to filter messages by primary agent for parallel agents and handle handoffs appropriately. - Enhanced message processing logic to ensure correct inclusion of agent content based on group and agent ID presence. - Improved documentation to clarify the distinctions between parallel execution and handoff scenarios. * feat: Implement primary agent ID selection for multi-agent content filtering * chore: Update @librechat/agents to version 3.0.63 in package.json and package-lock.json * chore: Update @librechat/agents to version 3.0.64 in package.json and package-lock.json * chore: Update @librechat/agents to version 3.0.65 in package.json and package-lock.json * feat: Add optional agent name to run creation for improved identification * chore: Update @librechat/agents to version 3.0.66 in package.json and package-lock.json * test: Add unit tests for edge utilities including key generation, participant extraction, and orphaned edge filtering - Implemented tests for `getEdgeKey`, `getEdgeParticipants`, `filterOrphanedEdges`, and `createEdgeCollector` functions. - Ensured comprehensive coverage for various edge cases, including handling of arrays and default values. - Verified correct behavior of edge filtering based on skipped agents and deduplication of edges.
This commit is contained in:
parent
d3b5020dd9
commit
791dab8f20
13 changed files with 521 additions and 71 deletions
|
|
@ -98,12 +98,42 @@ function logToolError(graph, error, toolId) {
|
|||
/** Regex pattern to match agent ID suffix (____N) */
|
||||
const AGENT_SUFFIX_PATTERN = /____(\d+)$/;
|
||||
|
||||
/**
|
||||
* Finds the primary agent ID within a set of agent IDs.
|
||||
* Primary = no suffix (____N) or lowest suffix number.
|
||||
* @param {Set<string>} agentIds
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function findPrimaryAgentId(agentIds) {
|
||||
let primaryAgentId = null;
|
||||
let lowestSuffixIndex = Infinity;
|
||||
|
||||
for (const agentId of agentIds) {
|
||||
const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN);
|
||||
if (!suffixMatch) {
|
||||
return agentId;
|
||||
}
|
||||
const suffixIndex = parseInt(suffixMatch[1], 10);
|
||||
if (suffixIndex < lowestSuffixIndex) {
|
||||
lowestSuffixIndex = suffixIndex;
|
||||
primaryAgentId = agentId;
|
||||
}
|
||||
}
|
||||
|
||||
return primaryAgentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mapMethod for getMessagesForConversation that processes agent content.
|
||||
* - Strips agentId/groupId metadata from all content
|
||||
* - For multi-agent: filters to primary agent content only (no suffix or lowest suffix)
|
||||
* - For parallel agents (addedConvo with groupId): filters each group to its primary agent
|
||||
* - For handoffs (agentId without groupId): keeps all content from all agents
|
||||
* - For multi-agent: applies agent labels to content
|
||||
*
|
||||
* The key distinction:
|
||||
* - Parallel execution (addedConvo): Parts have both agentId AND groupId
|
||||
* - Handoffs: Parts only have agentId, no groupId
|
||||
*
|
||||
* @param {Agent} primaryAgent - Primary agent configuration
|
||||
* @param {Map<string, Agent>} [agentConfigs] - Additional agent configurations
|
||||
* @returns {(message: TMessage) => TMessage} Map method for processing messages
|
||||
|
|
@ -127,40 +157,38 @@ function createMultiAgentMapper(primaryAgent, agentConfigs) {
|
|||
return message;
|
||||
}
|
||||
|
||||
// Find primary agent ID (no suffix, or lowest suffix number) - only needed for multi-agent
|
||||
let primaryAgentId = null;
|
||||
let hasAgentMetadata = false;
|
||||
|
||||
if (hasMultipleAgents) {
|
||||
let lowestSuffixIndex = Infinity;
|
||||
for (const part of message.content) {
|
||||
const agentId = part?.agentId;
|
||||
if (!agentId) {
|
||||
continue;
|
||||
}
|
||||
hasAgentMetadata = true;
|
||||
|
||||
const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN);
|
||||
if (!suffixMatch) {
|
||||
primaryAgentId = agentId;
|
||||
break;
|
||||
}
|
||||
const suffixIndex = parseInt(suffixMatch[1], 10);
|
||||
if (suffixIndex < lowestSuffixIndex) {
|
||||
lowestSuffixIndex = suffixIndex;
|
||||
primaryAgentId = agentId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single agent: just check if any metadata exists
|
||||
hasAgentMetadata = message.content.some((part) => part?.agentId || part?.groupId);
|
||||
}
|
||||
|
||||
// Check for metadata
|
||||
const hasAgentMetadata = message.content.some((part) => part?.agentId || part?.groupId != null);
|
||||
if (!hasAgentMetadata) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build a map of groupId -> Set of agentIds, to find primary per group
|
||||
/** @type {Map<number, Set<string>>} */
|
||||
const groupAgentMap = new Map();
|
||||
|
||||
for (const part of message.content) {
|
||||
const groupId = part?.groupId;
|
||||
const agentId = part?.agentId;
|
||||
if (groupId != null && agentId) {
|
||||
if (!groupAgentMap.has(groupId)) {
|
||||
groupAgentMap.set(groupId, new Set());
|
||||
}
|
||||
groupAgentMap.get(groupId).add(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
// For each group, find the primary agent
|
||||
/** @type {Map<number, string>} */
|
||||
const groupPrimaryMap = new Map();
|
||||
for (const [groupId, agentIds] of groupAgentMap) {
|
||||
const primary = findPrimaryAgentId(agentIds);
|
||||
if (primary) {
|
||||
groupPrimaryMap.set(groupId, primary);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Array<TMessageContentParts>} */
|
||||
const filteredContent = [];
|
||||
/** @type {Record<number, string>} */
|
||||
|
|
@ -168,8 +196,16 @@ function createMultiAgentMapper(primaryAgent, agentConfigs) {
|
|||
|
||||
for (const part of message.content) {
|
||||
const agentId = part?.agentId;
|
||||
// For single agent: include all parts; for multi-agent: filter to primary
|
||||
if (!hasMultipleAgents || !agentId || agentId === primaryAgentId) {
|
||||
const groupId = part?.groupId;
|
||||
|
||||
// Filtering logic:
|
||||
// - No groupId (handoffs): always include
|
||||
// - Has groupId (parallel): only include if it's the primary for that group
|
||||
const isParallelPart = groupId != null;
|
||||
const groupPrimary = isParallelPart ? groupPrimaryMap.get(groupId) : null;
|
||||
const shouldInclude = !isParallelPart || !agentId || agentId === groupPrimary;
|
||||
|
||||
if (shouldInclude) {
|
||||
const newIndex = filteredContent.length;
|
||||
const { agentId: _a, groupId: _g, ...cleanPart } = part;
|
||||
filteredContent.push(cleanPart);
|
||||
|
|
@ -325,11 +361,14 @@ class AgentClient extends BaseClient {
|
|||
{ instructions = null, additional_instructions = null },
|
||||
opts,
|
||||
) {
|
||||
const hasAddedConvo = this.options.req?.body?.addedConvo != null;
|
||||
const orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
parentMessageId,
|
||||
summary: this.shouldSummarize,
|
||||
mapMethod: createMultiAgentMapper(this.options.agent, this.agentConfigs),
|
||||
mapMethod: hasAddedConvo
|
||||
? createMultiAgentMapper(this.options.agent, this.agentConfigs)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
let payload;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ const {
|
|||
validateAgentModel,
|
||||
getCustomEndpointConfig,
|
||||
createSequentialChainEdges,
|
||||
createEdgeCollector,
|
||||
filterOrphanedEdges,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
|
|
@ -143,10 +145,17 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
|
|||
const agent_ids = primaryConfig.agent_ids;
|
||||
let userMCPAuthMap = primaryConfig.userMCPAuthMap;
|
||||
|
||||
/** @type {Set<string>} Track agents that failed to load (orphaned references) */
|
||||
const skippedAgentIds = new Set();
|
||||
|
||||
async function processAgent(agentId) {
|
||||
const agent = await getAgent({ id: agentId });
|
||||
if (!agent) {
|
||||
throw new Error(`Agent ${agentId} not found`);
|
||||
logger.warn(
|
||||
`[processAgent] Handoff agent ${agentId} not found, skipping (orphaned reference)`,
|
||||
);
|
||||
skippedAgentIds.add(agentId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const validationResult = await validateAgentModel({
|
||||
|
|
@ -187,37 +196,31 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
|
|||
userMCPAuthMap = config.userMCPAuthMap;
|
||||
}
|
||||
agentConfigs.set(agentId, config);
|
||||
return agent;
|
||||
}
|
||||
|
||||
let edges = primaryConfig.edges;
|
||||
const checkAgentInit = (agentId) => agentId === primaryConfig.id || agentConfigs.has(agentId);
|
||||
if ((edges?.length ?? 0) > 0) {
|
||||
for (const edge of edges) {
|
||||
if (Array.isArray(edge.to)) {
|
||||
for (const to of edge.to) {
|
||||
if (checkAgentInit(to)) {
|
||||
continue;
|
||||
}
|
||||
await processAgent(to);
|
||||
}
|
||||
} else if (typeof edge.to === 'string' && checkAgentInit(edge.to)) {
|
||||
continue;
|
||||
} else if (typeof edge.to === 'string') {
|
||||
await processAgent(edge.to);
|
||||
}
|
||||
|
||||
if (Array.isArray(edge.from)) {
|
||||
for (const from of edge.from) {
|
||||
if (checkAgentInit(from)) {
|
||||
continue;
|
||||
}
|
||||
await processAgent(from);
|
||||
}
|
||||
} else if (typeof edge.from === 'string' && checkAgentInit(edge.from)) {
|
||||
continue;
|
||||
} else if (typeof edge.from === 'string') {
|
||||
await processAgent(edge.from);
|
||||
// Graph topology discovery for recursive agent handoffs (BFS)
|
||||
const { edgeMap, agentsToProcess, collectEdges } = createEdgeCollector(
|
||||
checkAgentInit,
|
||||
skippedAgentIds,
|
||||
);
|
||||
|
||||
// Seed with primary agent's edges
|
||||
collectEdges(primaryConfig.edges);
|
||||
|
||||
// BFS to load and merge all connected agents (enables transitive handoffs: A->B->C)
|
||||
while (agentsToProcess.size > 0) {
|
||||
const agentId = agentsToProcess.values().next().value;
|
||||
agentsToProcess.delete(agentId);
|
||||
try {
|
||||
const agent = await processAgent(agentId);
|
||||
if (agent?.edges?.length) {
|
||||
collectEdges(agent.edges);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[initializeClient] Error processing agent ${agentId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,11 +232,12 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
|
|||
}
|
||||
await processAgent(agentId);
|
||||
}
|
||||
|
||||
const chain = await createSequentialChainEdges([primaryConfig.id].concat(agent_ids), '{convo}');
|
||||
edges = edges ? edges.concat(chain) : chain;
|
||||
collectEdges(chain);
|
||||
}
|
||||
|
||||
let edges = Array.from(edgeMap.values());
|
||||
|
||||
/** Multi-Convo: Process addedConvo for parallel agent execution */
|
||||
const { userMCPAuthMap: updatedMCPAuthMap } = await processAddedConvo({
|
||||
req,
|
||||
|
|
@ -261,6 +265,9 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
|
|||
edges = [];
|
||||
}
|
||||
|
||||
// Filter out edges referencing non-existent agents (orphaned references)
|
||||
edges = filterOrphanedEdges(edges, skippedAgentIds);
|
||||
|
||||
primaryConfig.edges = edges;
|
||||
|
||||
let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue