mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-24 11:16:12 +01:00
🧑🏫 fix: Multi-Agent Instructions Handling (#11484)
* 🧑🏫 fix: Multi-Agent Instructions Handling
* Refactored AgentClient to streamline the process of building messages by applying shared run context and agent-specific instructions.
* Introduced new utility functions in context.ts for extracting MCP server names, fetching MCP instructions, and building combined agent instructions.
* Updated the Agent type to make instructions optional, allowing for more flexible agent configurations.
* Improved the handling of context application to agents, ensuring that all relevant information is correctly integrated before execution.
* chore: Update EphemeralAgent Type in Context
* Enhanced the context.ts file by importing the TEphemeralAgent type from librechat-data-provider.
* Updated the applyContextToAgent function to use TEphemeralAgent for the ephemeralAgent parameter, improving type safety and clarity in agent context handling.
* ci: Update Agent Instructions in Tests for Clarity
* Revised test assertions in AgentClient to clarify the source of agent instructions, ensuring they are explicitly referenced as coming from agent configuration rather than build options.
* Updated comments in tests to enhance understanding of the expected behavior regarding base agent instructions and their handling in various scenarios.
* ci: Unit Tests for Agent Context Utilities
* Introduced comprehensive unit tests for agent context utilities, including functions for extracting MCP servers, fetching MCP instructions, and building agent instructions.
* Enhanced test coverage to ensure correct behavior across various scenarios, including handling of empty tools, mixed tool types, and error cases.
* Improved type definitions for AgentWithTools to clarify the structure and requirements for agent context operations.
This commit is contained in:
parent
7204e74390
commit
cfd5c793a9
6 changed files with 758 additions and 86 deletions
|
|
@ -1,6 +1,5 @@
|
|||
require('events').EventEmitter.defaultMaxListeners = 100;
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { DynamicStructuredTool } = require('@langchain/core/tools');
|
||||
const { getBufferString, HumanMessage } = require('@langchain/core/messages');
|
||||
const {
|
||||
createRun,
|
||||
|
|
@ -14,6 +13,7 @@ const {
|
|||
getBalanceConfig,
|
||||
getProviderConfig,
|
||||
memoryInstructions,
|
||||
applyContextToAgent,
|
||||
GenerationJobManager,
|
||||
getTransactionsConfig,
|
||||
createMemoryProcessor,
|
||||
|
|
@ -328,11 +328,13 @@ class AgentClient extends BaseClient {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns build message options. For AgentClient, agent-specific instructions
|
||||
* are retrieved directly from agent objects in buildMessages, so this returns empty.
|
||||
* @returns {Object} Empty options object
|
||||
*/
|
||||
getBuildMessagesOptions() {
|
||||
return {
|
||||
instructions: this.options.agent.instructions,
|
||||
additional_instructions: this.options.agent.additional_instructions,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -355,12 +357,7 @@ class AgentClient extends BaseClient {
|
|||
return files;
|
||||
}
|
||||
|
||||
async buildMessages(
|
||||
messages,
|
||||
parentMessageId,
|
||||
{ instructions = null, additional_instructions = null },
|
||||
opts,
|
||||
) {
|
||||
async buildMessages(messages, parentMessageId, _buildOptions, opts) {
|
||||
/** Always pass mapMethod; getMessagesForConversation applies it only to messages with addedConvo flag */
|
||||
const orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
|
|
@ -374,11 +371,29 @@ class AgentClient extends BaseClient {
|
|||
/** @type {number | undefined} */
|
||||
let promptTokens;
|
||||
|
||||
/** @type {string} */
|
||||
let systemContent = [instructions ?? '', additional_instructions ?? '']
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.trim();
|
||||
/**
|
||||
* Extract base instructions for all agents (combines instructions + additional_instructions).
|
||||
* This must be done before applying context to preserve the original agent configuration.
|
||||
*/
|
||||
const extractBaseInstructions = (agent) => {
|
||||
const baseInstructions = [agent.instructions ?? '', agent.additional_instructions ?? '']
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.trim();
|
||||
agent.instructions = baseInstructions;
|
||||
return agent;
|
||||
};
|
||||
|
||||
/** Collect all agents for unified processing, extracting base instructions during collection */
|
||||
const allAgents = [
|
||||
{ agent: extractBaseInstructions(this.options.agent), agentId: this.options.agent.id },
|
||||
...(this.agentConfigs?.size > 0
|
||||
? Array.from(this.agentConfigs.entries()).map(([agentId, agent]) => ({
|
||||
agent: extractBaseInstructions(agent),
|
||||
agentId,
|
||||
}))
|
||||
: []),
|
||||
];
|
||||
|
||||
if (this.options.attachments) {
|
||||
const attachments = await this.options.attachments;
|
||||
|
|
@ -413,6 +428,7 @@ class AgentClient extends BaseClient {
|
|||
assistantName: this.options?.modelLabel,
|
||||
});
|
||||
|
||||
/** For non-latest messages, prepend file context directly to message content */
|
||||
if (message.fileContext && i !== orderedMessages.length - 1) {
|
||||
if (typeof formattedMessage.content === 'string') {
|
||||
formattedMessage.content = message.fileContext + '\n' + formattedMessage.content;
|
||||
|
|
@ -422,8 +438,6 @@ class AgentClient extends BaseClient {
|
|||
? (textPart.text = message.fileContext + '\n' + textPart.text)
|
||||
: formattedMessage.content.unshift({ type: 'text', text: message.fileContext });
|
||||
}
|
||||
} else if (message.fileContext && i === orderedMessages.length - 1) {
|
||||
systemContent = [systemContent, message.fileContext].join('\n');
|
||||
}
|
||||
|
||||
const needsTokenCount =
|
||||
|
|
@ -456,46 +470,35 @@ class AgentClient extends BaseClient {
|
|||
return formattedMessage;
|
||||
});
|
||||
|
||||
/**
|
||||
* Build shared run context - applies to ALL agents in the run.
|
||||
* This includes: file context (latest message), augmented prompt (RAG), memory context.
|
||||
*/
|
||||
const sharedRunContextParts = [];
|
||||
|
||||
/** File context from the latest message (attachments) */
|
||||
const latestMessage = orderedMessages[orderedMessages.length - 1];
|
||||
if (latestMessage?.fileContext) {
|
||||
sharedRunContextParts.push(latestMessage.fileContext);
|
||||
}
|
||||
|
||||
/** Augmented prompt from RAG/context handlers */
|
||||
if (this.contextHandlers) {
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
systemContent = this.augmentedPrompt + systemContent;
|
||||
}
|
||||
|
||||
// Inject MCP server instructions if available
|
||||
const ephemeralAgent = this.options.req.body.ephemeralAgent;
|
||||
let mcpServers = [];
|
||||
|
||||
// Check for ephemeral agent MCP servers
|
||||
if (ephemeralAgent && ephemeralAgent.mcp && ephemeralAgent.mcp.length > 0) {
|
||||
mcpServers = ephemeralAgent.mcp;
|
||||
}
|
||||
// Check for regular agent MCP tools
|
||||
else if (this.options.agent && this.options.agent.tools) {
|
||||
mcpServers = this.options.agent.tools
|
||||
.filter(
|
||||
(tool) =>
|
||||
tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter),
|
||||
)
|
||||
.map((tool) => tool.name.split(Constants.mcp_delimiter).pop())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (mcpServers.length > 0) {
|
||||
try {
|
||||
const mcpInstructions = await getMCPManager().formatInstructionsForContext(mcpServers);
|
||||
if (mcpInstructions) {
|
||||
systemContent = [systemContent, mcpInstructions].filter(Boolean).join('\n\n');
|
||||
logger.debug('[AgentClient] Injected MCP instructions for servers:', mcpServers);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[AgentClient] Failed to inject MCP instructions:', error);
|
||||
if (this.augmentedPrompt) {
|
||||
sharedRunContextParts.push(this.augmentedPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
if (systemContent) {
|
||||
this.options.agent.instructions = systemContent;
|
||||
/** Memory context (user preferences/memories) */
|
||||
const withoutKeys = await this.useMemory();
|
||||
if (withoutKeys) {
|
||||
const memoryContext = `${memoryInstructions}\n\n# Existing memory about the user:\n${withoutKeys}`;
|
||||
sharedRunContextParts.push(memoryContext);
|
||||
}
|
||||
|
||||
const sharedRunContext = sharedRunContextParts.join('\n\n');
|
||||
|
||||
/** @type {Record<string, number> | undefined} */
|
||||
let tokenCountMap;
|
||||
|
||||
|
|
@ -521,36 +524,27 @@ class AgentClient extends BaseClient {
|
|||
opts.getReqData({ promptTokens });
|
||||
}
|
||||
|
||||
const withoutKeys = await this.useMemory();
|
||||
const memoryContext = withoutKeys
|
||||
? `${memoryInstructions}\n\n# Existing memory about the user:\n${withoutKeys}`
|
||||
: '';
|
||||
if (memoryContext) {
|
||||
systemContent += memoryContext;
|
||||
}
|
||||
|
||||
if (systemContent) {
|
||||
this.options.agent.instructions = systemContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass memory context to parallel agents (addedConvo) so they have the same user context.
|
||||
* Apply context to all agents.
|
||||
* Each agent gets: shared run context + their own base instructions + their own MCP instructions.
|
||||
*
|
||||
* NOTE: This intentionally mutates the agentConfig objects in place. The agentConfigs Map
|
||||
* holds references to config objects that will be passed to the graph runtime. Mutating
|
||||
* them here ensures all parallel agents receive the memory context before execution starts.
|
||||
* Creating new objects would not work because the Map references would still point to the old objects.
|
||||
* NOTE: This intentionally mutates agent objects in place. The agentConfigs Map
|
||||
* holds references to config objects that will be passed to the graph runtime.
|
||||
*/
|
||||
if (memoryContext && this.agentConfigs?.size > 0) {
|
||||
for (const [agentId, agentConfig] of this.agentConfigs.entries()) {
|
||||
if (agentConfig.instructions) {
|
||||
agentConfig.instructions = agentConfig.instructions + '\n\n' + memoryContext;
|
||||
} else {
|
||||
agentConfig.instructions = memoryContext;
|
||||
}
|
||||
logger.debug(`[AgentClient] Added memory context to parallel agent: ${agentId}`);
|
||||
}
|
||||
}
|
||||
const ephemeralAgent = this.options.req.body.ephemeralAgent;
|
||||
const mcpManager = getMCPManager();
|
||||
await Promise.all(
|
||||
allAgents.map(({ agent, agentId }) =>
|
||||
applyContextToAgent({
|
||||
agent,
|
||||
agentId,
|
||||
logger,
|
||||
mcpManager,
|
||||
sharedRunContext,
|
||||
ephemeralAgent: agentId === this.options.agent.id ? ephemeralAgent : undefined,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1310,8 +1310,8 @@ describe('AgentClient - titleConvo', () => {
|
|||
expect(client.options.agent.instructions).toContain('# MCP Server Instructions');
|
||||
expect(client.options.agent.instructions).toContain('Use these tools carefully');
|
||||
|
||||
// Verify the base instructions are also included
|
||||
expect(client.options.agent.instructions).toContain('Base instructions');
|
||||
// Verify the base instructions are also included (from agent config, not buildOptions)
|
||||
expect(client.options.agent.instructions).toContain('Base agent instructions');
|
||||
});
|
||||
|
||||
it('should handle MCP instructions with ephemeral agent', async () => {
|
||||
|
|
@ -1373,8 +1373,8 @@ describe('AgentClient - titleConvo', () => {
|
|||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Verify the instructions still work without MCP content
|
||||
expect(client.options.agent.instructions).toBe('Base instructions only');
|
||||
// Verify the instructions still work without MCP content (from agent config, not buildOptions)
|
||||
expect(client.options.agent.instructions).toBe('Base agent instructions');
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
});
|
||||
|
||||
|
|
@ -1398,8 +1398,8 @@ describe('AgentClient - titleConvo', () => {
|
|||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Should still have base instructions without MCP content
|
||||
expect(client.options.agent.instructions).toContain('Base instructions');
|
||||
// Should still have base instructions without MCP content (from agent config, not buildOptions)
|
||||
expect(client.options.agent.instructions).toContain('Base agent instructions');
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
});
|
||||
});
|
||||
|
|
@ -1945,7 +1945,8 @@ describe('AgentClient - titleConvo', () => {
|
|||
|
||||
expect(client.useMemory).toHaveBeenCalled();
|
||||
|
||||
expect(client.options.agent.instructions).toContain('Base instructions');
|
||||
// Verify primary agent has its configured instructions (not from buildOptions) and memory context
|
||||
expect(client.options.agent.instructions).toContain('Primary agent instructions');
|
||||
expect(client.options.agent.instructions).toContain(memoryContent);
|
||||
|
||||
expect(parallelAgent1.instructions).toContain('Parallel agent 1 instructions');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue