mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🔧 fix: Await MCP Instructions and Filter Malformed Tool Calls (#10485)
* fix: Await MCP instructions formatting in AgentClient * fix: don't render or aggregate malformed tool calls * fix: implement filter for malformed tool call content parts and add tests
This commit is contained in:
parent
aff3cd3667
commit
cabc8afeac
10 changed files with 467 additions and 9 deletions
|
|
@ -47,7 +47,7 @@
|
|||
"@langchain/google-genai": "^0.2.13",
|
||||
"@langchain/google-vertexai": "^0.2.13",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^3.0.15",
|
||||
"@librechat/agents": "^3.0.17",
|
||||
"@librechat/api": "*",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const {
|
|||
memoryInstructions,
|
||||
getTransactionsConfig,
|
||||
createMemoryProcessor,
|
||||
filterMalformedContentParts,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
Callback,
|
||||
|
|
@ -344,7 +345,7 @@ class AgentClient extends BaseClient {
|
|||
|
||||
if (mcpServers.length > 0) {
|
||||
try {
|
||||
const mcpInstructions = getMCPManager().formatInstructionsForContext(mcpServers);
|
||||
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);
|
||||
|
|
@ -611,7 +612,7 @@ class AgentClient extends BaseClient {
|
|||
userMCPAuthMap: opts.userMCPAuthMap,
|
||||
abortController: opts.abortController,
|
||||
});
|
||||
return this.contentParts;
|
||||
return filterMalformedContentParts(this.contentParts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ jest.mock('@librechat/api', () => ({
|
|||
...jest.requireActual('@librechat/api'),
|
||||
}));
|
||||
|
||||
// Mock getMCPManager
|
||||
const mockFormatInstructions = jest.fn();
|
||||
jest.mock('~/config', () => ({
|
||||
getMCPManager: jest.fn(() => ({
|
||||
formatInstructionsForContext: mockFormatInstructions,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('AgentClient - titleConvo', () => {
|
||||
let client;
|
||||
let mockRun;
|
||||
|
|
@ -1168,6 +1176,200 @@ describe('AgentClient - titleConvo', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('buildMessages with MCP server instructions', () => {
|
||||
let client;
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
let mockAgent;
|
||||
let mockOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset the mock to default behavior
|
||||
mockFormatInstructions.mockResolvedValue(
|
||||
'# MCP Server Instructions\n\nTest MCP instructions here',
|
||||
);
|
||||
|
||||
const { DynamicStructuredTool } = require('@langchain/core/tools');
|
||||
|
||||
// Create mock MCP tools with the delimiter pattern
|
||||
const mockMCPTool1 = new DynamicStructuredTool({
|
||||
name: `tool1${Constants.mcp_delimiter}server1`,
|
||||
description: 'Test MCP tool 1',
|
||||
schema: {},
|
||||
func: async () => 'result',
|
||||
});
|
||||
|
||||
const mockMCPTool2 = new DynamicStructuredTool({
|
||||
name: `tool2${Constants.mcp_delimiter}server2`,
|
||||
description: 'Test MCP tool 2',
|
||||
schema: {},
|
||||
func: async () => 'result',
|
||||
});
|
||||
|
||||
mockAgent = {
|
||||
id: 'agent-123',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
provider: EModelEndpoint.openAI,
|
||||
instructions: 'Base agent instructions',
|
||||
model_parameters: {
|
||||
model: 'gpt-4',
|
||||
},
|
||||
tools: [mockMCPTool1, mockMCPTool2],
|
||||
};
|
||||
|
||||
mockReq = {
|
||||
user: {
|
||||
id: 'user-123',
|
||||
},
|
||||
body: {
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
},
|
||||
config: {},
|
||||
};
|
||||
|
||||
mockRes = {};
|
||||
|
||||
mockOptions = {
|
||||
req: mockReq,
|
||||
res: mockRes,
|
||||
agent: mockAgent,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
};
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.conversationId = 'convo-123';
|
||||
client.responseMessageId = 'response-123';
|
||||
client.shouldSummarize = false;
|
||||
client.maxContextTokens = 4096;
|
||||
});
|
||||
|
||||
it('should await MCP instructions and not include [object Promise] in agent instructions', async () => {
|
||||
// Set specific return value for this test
|
||||
mockFormatInstructions.mockResolvedValue(
|
||||
'# MCP Server Instructions\n\nUse these tools carefully',
|
||||
);
|
||||
|
||||
const messages = [
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
];
|
||||
|
||||
await client.buildMessages(messages, null, {
|
||||
instructions: 'Base instructions',
|
||||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Verify formatInstructionsForContext was called with correct server names
|
||||
expect(mockFormatInstructions).toHaveBeenCalledWith(['server1', 'server2']);
|
||||
|
||||
// Verify the instructions do NOT contain [object Promise]
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
|
||||
// Verify the instructions DO contain the MCP instructions
|
||||
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');
|
||||
});
|
||||
|
||||
it('should handle MCP instructions with ephemeral agent', async () => {
|
||||
// Set specific return value for this test
|
||||
mockFormatInstructions.mockResolvedValue(
|
||||
'# Ephemeral MCP Instructions\n\nSpecial ephemeral instructions',
|
||||
);
|
||||
|
||||
// Set up ephemeral agent with MCP servers
|
||||
mockReq.body.ephemeralAgent = {
|
||||
mcp: ['ephemeral-server1', 'ephemeral-server2'],
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Test ephemeral',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
];
|
||||
|
||||
await client.buildMessages(messages, null, {
|
||||
instructions: 'Ephemeral instructions',
|
||||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Verify formatInstructionsForContext was called with ephemeral server names
|
||||
expect(mockFormatInstructions).toHaveBeenCalledWith([
|
||||
'ephemeral-server1',
|
||||
'ephemeral-server2',
|
||||
]);
|
||||
|
||||
// Verify no [object Promise] in instructions
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
|
||||
// Verify ephemeral MCP instructions are included
|
||||
expect(client.options.agent.instructions).toContain('# Ephemeral MCP Instructions');
|
||||
expect(client.options.agent.instructions).toContain('Special ephemeral instructions');
|
||||
});
|
||||
|
||||
it('should handle empty MCP instructions gracefully', async () => {
|
||||
// Set empty return value for this test
|
||||
mockFormatInstructions.mockResolvedValue('');
|
||||
|
||||
const messages = [
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
];
|
||||
|
||||
await client.buildMessages(messages, null, {
|
||||
instructions: 'Base instructions only',
|
||||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Verify the instructions still work without MCP content
|
||||
expect(client.options.agent.instructions).toBe('Base instructions only');
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
});
|
||||
|
||||
it('should handle MCP instructions error gracefully', async () => {
|
||||
// Set error return for this test
|
||||
mockFormatInstructions.mockRejectedValue(new Error('MCP error'));
|
||||
|
||||
const messages = [
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Should not throw
|
||||
await client.buildMessages(messages, null, {
|
||||
instructions: 'Base instructions',
|
||||
additional_instructions: null,
|
||||
});
|
||||
|
||||
// Should still have base instructions without MCP content
|
||||
expect(client.options.agent.instructions).toContain('Base instructions');
|
||||
expect(client.options.agent.instructions).not.toContain('[object Promise]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMemory method', () => {
|
||||
let client;
|
||||
let mockReq;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue