@librechat/agents 3.0.27 → 3.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +68 -2
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +10 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +68 -2
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +10 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/package.json +2 -1
- package/src/llm/anthropic/utils/message_inputs.ts +94 -9
- package/src/scripts/ant_web_search_edge_case.ts +162 -0
- package/src/specs/anthropic.simple.test.ts +67 -0
- package/src/stream.ts +4 -1
- package/src/tools/ToolNode.ts +12 -1
|
@@ -363,6 +363,78 @@ function _formatContent(message: BaseMessage) {
|
|
|
363
363
|
return content;
|
|
364
364
|
} else {
|
|
365
365
|
const contentBlocks = content.map((contentPart) => {
|
|
366
|
+
/**
|
|
367
|
+
* Handle malformed blocks that have server tool fields mixed with text type.
|
|
368
|
+
* These can occur when server_tool_use blocks get mislabeled during aggregation.
|
|
369
|
+
* Correct their type ONLY if we can confirm it's a server tool by checking the ID prefix.
|
|
370
|
+
* Anthropic needs both server_tool_use and web_search_tool_result blocks for citations to work.
|
|
371
|
+
*/
|
|
372
|
+
if (
|
|
373
|
+
'id' in contentPart &&
|
|
374
|
+
'name' in contentPart &&
|
|
375
|
+
'input' in contentPart &&
|
|
376
|
+
contentPart.type === 'text'
|
|
377
|
+
) {
|
|
378
|
+
const rawPart = contentPart as Record<string, unknown>;
|
|
379
|
+
const id = rawPart.id as string;
|
|
380
|
+
|
|
381
|
+
// Only correct if this is definitely a server tool (ID starts with 'srvtoolu_')
|
|
382
|
+
if (id && id.startsWith('srvtoolu_')) {
|
|
383
|
+
let input = rawPart.input;
|
|
384
|
+
|
|
385
|
+
// Ensure input is an object
|
|
386
|
+
if (typeof input === 'string') {
|
|
387
|
+
try {
|
|
388
|
+
input = JSON.parse(input);
|
|
389
|
+
} catch {
|
|
390
|
+
input = {};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const corrected: AnthropicServerToolUseBlockParam = {
|
|
395
|
+
type: 'server_tool_use',
|
|
396
|
+
id,
|
|
397
|
+
name: 'web_search',
|
|
398
|
+
input: input as Record<string, unknown>,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return corrected;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If it's not a server tool, skip it (return null to filter it out)
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Handle malformed web_search_tool_result blocks marked as text.
|
|
410
|
+
* These have tool_use_id and nested content arrays - fix their type instead of filtering.
|
|
411
|
+
* Only correct if we can confirm it's a web search result by checking the tool_use_id prefix.
|
|
412
|
+
*/
|
|
413
|
+
if (
|
|
414
|
+
'tool_use_id' in contentPart &&
|
|
415
|
+
'content' in contentPart &&
|
|
416
|
+
Array.isArray(contentPart.content) &&
|
|
417
|
+
contentPart.type === 'text'
|
|
418
|
+
) {
|
|
419
|
+
const rawPart = contentPart as Record<string, unknown>;
|
|
420
|
+
const toolUseId = rawPart.tool_use_id as string;
|
|
421
|
+
|
|
422
|
+
// Only correct if this is definitely a server tool result (tool_use_id starts with 'srvtoolu_')
|
|
423
|
+
if (toolUseId && toolUseId.startsWith('srvtoolu_')) {
|
|
424
|
+
const corrected: AnthropicWebSearchToolResultBlockParam = {
|
|
425
|
+
type: 'web_search_tool_result',
|
|
426
|
+
tool_use_id: toolUseId,
|
|
427
|
+
content:
|
|
428
|
+
rawPart.content as AnthropicWebSearchToolResultBlockParam['content'],
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return corrected;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// If it's not a server tool result, skip it (return null to filter it out)
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
366
438
|
if (isDataContentBlock(contentPart)) {
|
|
367
439
|
return convertToProviderContentBlock(
|
|
368
440
|
contentPart,
|
|
@@ -459,6 +531,14 @@ function _formatContent(message: BaseMessage) {
|
|
|
459
531
|
}
|
|
460
532
|
}
|
|
461
533
|
|
|
534
|
+
/**
|
|
535
|
+
* For multi-turn conversations with citations, we must preserve ALL blocks
|
|
536
|
+
* including server_tool_use, web_search_tool_result, and web_search_result.
|
|
537
|
+
* Citations reference search results by index, so filtering changes indices and breaks references.
|
|
538
|
+
*
|
|
539
|
+
* The ToolNode already handles skipping server tool invocations via the srvtoolu_ prefix check.
|
|
540
|
+
*/
|
|
541
|
+
|
|
462
542
|
// TODO: Fix when SDK types are fixed
|
|
463
543
|
return {
|
|
464
544
|
...contentPartCopy,
|
|
@@ -487,10 +567,14 @@ function _formatContent(message: BaseMessage) {
|
|
|
487
567
|
input: contentPart.functionCall.args,
|
|
488
568
|
};
|
|
489
569
|
} else {
|
|
570
|
+
console.error(
|
|
571
|
+
'Unsupported content part:',
|
|
572
|
+
JSON.stringify(contentPart, null, 2)
|
|
573
|
+
);
|
|
490
574
|
throw new Error('Unsupported message content format');
|
|
491
575
|
}
|
|
492
576
|
});
|
|
493
|
-
return contentBlocks;
|
|
577
|
+
return contentBlocks.filter((block) => block !== null);
|
|
494
578
|
}
|
|
495
579
|
}
|
|
496
580
|
|
|
@@ -545,14 +629,15 @@ export function _convertMessagesToAnthropicPayload(
|
|
|
545
629
|
}
|
|
546
630
|
} else {
|
|
547
631
|
const { content } = message;
|
|
548
|
-
const hasMismatchedToolCalls = !message.tool_calls.every(
|
|
549
|
-
|
|
550
|
-
(
|
|
551
|
-
(contentPart
|
|
552
|
-
contentPart.type === '
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
632
|
+
const hasMismatchedToolCalls = !message.tool_calls.every(
|
|
633
|
+
(toolCall) =>
|
|
634
|
+
!!content.find(
|
|
635
|
+
(contentPart) =>
|
|
636
|
+
(contentPart.type === 'tool_use' ||
|
|
637
|
+
contentPart.type === 'input_json_delta' ||
|
|
638
|
+
contentPart.type === 'server_tool_use') &&
|
|
639
|
+
contentPart.id === toolCall.id
|
|
640
|
+
)
|
|
556
641
|
);
|
|
557
642
|
if (hasMismatchedToolCalls) {
|
|
558
643
|
console.warn(
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// src/scripts/cli.ts
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
config();
|
|
5
|
+
import { HumanMessage, BaseMessage } from '@langchain/core/messages';
|
|
6
|
+
import type * as t from '@/types';
|
|
7
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
8
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
9
|
+
import { Calculator } from '@/tools/Calculator';
|
|
10
|
+
|
|
11
|
+
import { getArgs } from '@/scripts/args';
|
|
12
|
+
import { Run } from '@/run';
|
|
13
|
+
import { GraphEvents, Callback, Providers } from '@/common';
|
|
14
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
15
|
+
|
|
16
|
+
const conversationHistory: BaseMessage[] = [];
|
|
17
|
+
let _contentParts: (t.MessageContentComplex | undefined)[] = [];
|
|
18
|
+
async function testStandardStreaming(): Promise<void> {
|
|
19
|
+
const { userName, location, currentDate } = await getArgs();
|
|
20
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
21
|
+
_contentParts = contentParts;
|
|
22
|
+
const customHandlers = {
|
|
23
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
24
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
25
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
26
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
27
|
+
handle: (
|
|
28
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
29
|
+
data: t.StreamEventData
|
|
30
|
+
): void => {
|
|
31
|
+
console.log('====== ON_RUN_STEP_COMPLETED ======');
|
|
32
|
+
// console.dir(data, { depth: null });
|
|
33
|
+
aggregateContent({
|
|
34
|
+
event,
|
|
35
|
+
data: data as unknown as { result: t.ToolEndEvent },
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
40
|
+
handle: (
|
|
41
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
42
|
+
data: t.StreamEventData
|
|
43
|
+
): void => {
|
|
44
|
+
console.log('====== ON_RUN_STEP ======');
|
|
45
|
+
console.dir(data, { depth: null });
|
|
46
|
+
aggregateContent({ event, data: data as t.RunStep });
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
50
|
+
handle: (
|
|
51
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
52
|
+
data: t.StreamEventData
|
|
53
|
+
): void => {
|
|
54
|
+
console.log('====== ON_RUN_STEP_DELTA ======');
|
|
55
|
+
console.dir(data, { depth: null });
|
|
56
|
+
aggregateContent({ event, data: data as t.RunStepDeltaEvent });
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
60
|
+
handle: (
|
|
61
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
62
|
+
data: t.StreamEventData
|
|
63
|
+
): void => {
|
|
64
|
+
// console.log('====== ON_MESSAGE_DELTA ======');
|
|
65
|
+
// console.dir(data, { depth: null });
|
|
66
|
+
aggregateContent({ event, data: data as t.MessageDeltaEvent });
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
[GraphEvents.TOOL_START]: {
|
|
70
|
+
handle: (
|
|
71
|
+
_event: string,
|
|
72
|
+
data: t.StreamEventData,
|
|
73
|
+
metadata?: Record<string, unknown>
|
|
74
|
+
): void => {
|
|
75
|
+
console.log('====== TOOL_START ======');
|
|
76
|
+
// console.dir(data, { depth: null });
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const llmConfig = getLLMConfig(
|
|
82
|
+
Providers.ANTHROPIC
|
|
83
|
+
) as t.AnthropicClientOptions & t.SharedLLMConfig;
|
|
84
|
+
llmConfig.model = 'claude-haiku-4-5';
|
|
85
|
+
|
|
86
|
+
const run = await Run.create<t.IState>({
|
|
87
|
+
runId: 'test-run-id',
|
|
88
|
+
graphConfig: {
|
|
89
|
+
type: 'standard',
|
|
90
|
+
llmConfig,
|
|
91
|
+
tools: [
|
|
92
|
+
{
|
|
93
|
+
type: 'web_search_20250305',
|
|
94
|
+
name: 'web_search',
|
|
95
|
+
max_uses: 5,
|
|
96
|
+
},
|
|
97
|
+
new Calculator(),
|
|
98
|
+
],
|
|
99
|
+
instructions: 'You are a friendly AI assistant.',
|
|
100
|
+
// additional_instructions: `Always address the user by their name. The user's name is ${userName} and they are located in ${location}.`,
|
|
101
|
+
},
|
|
102
|
+
returnContent: true,
|
|
103
|
+
customHandlers,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const config = {
|
|
107
|
+
configurable: {
|
|
108
|
+
provider: Providers.ANTHROPIC,
|
|
109
|
+
thread_id: 'conversation-num-1',
|
|
110
|
+
},
|
|
111
|
+
streamMode: 'values',
|
|
112
|
+
version: 'v2' as const,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
console.log('Test 1: Web search + calculator (simultaneous tool test)');
|
|
116
|
+
|
|
117
|
+
// const userMessage = `
|
|
118
|
+
// Make a search for the weather in ${location} today, which is ${currentDate}.
|
|
119
|
+
// Before making the search, please let me know what you're about to do, then immediately start searching without hesitation.
|
|
120
|
+
// Make sure to always refer to me by name, which is ${userName}.
|
|
121
|
+
// After giving me a thorough summary, tell me a joke about the weather forecast we went over.
|
|
122
|
+
// `;
|
|
123
|
+
// const userMessage = 'Are massage guns good?';
|
|
124
|
+
// const userMessage = 'What is functional programming?';
|
|
125
|
+
// const userMessage = "Get me today's trending news.";
|
|
126
|
+
// const userMessage = "search recent italy earthquake volcano activity";
|
|
127
|
+
// const userMessage =
|
|
128
|
+
// "use 'Trump' as the exact search query and tell me what you find.";
|
|
129
|
+
const userMessage =
|
|
130
|
+
'Can you search the web for the current population of Tokyo, and also calculate what 15% of that population would be? Do both at the same time.';
|
|
131
|
+
|
|
132
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
133
|
+
|
|
134
|
+
const inputs = {
|
|
135
|
+
messages: conversationHistory,
|
|
136
|
+
};
|
|
137
|
+
const finalContentParts = await run.processStream(inputs, config);
|
|
138
|
+
const finalMessages = run.getRunMessages();
|
|
139
|
+
if (finalMessages) {
|
|
140
|
+
conversationHistory.push(...finalMessages);
|
|
141
|
+
console.dir(conversationHistory, { depth: null });
|
|
142
|
+
}
|
|
143
|
+
// console.dir(finalContentParts, { depth: null });
|
|
144
|
+
console.log('\n\n====================\n\n');
|
|
145
|
+
// console.dir(contentParts, { depth: null });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
149
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
150
|
+
console.log('Content Parts:');
|
|
151
|
+
console.dir(_contentParts, { depth: null });
|
|
152
|
+
process.exit(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
testStandardStreaming().catch((err) => {
|
|
156
|
+
console.error(err);
|
|
157
|
+
console.log('Conversation history:');
|
|
158
|
+
console.dir(conversationHistory, { depth: null });
|
|
159
|
+
console.log('Content Parts:');
|
|
160
|
+
console.dir(_contentParts, { depth: null });
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
@@ -306,6 +306,73 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
306
306
|
expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
|
|
307
307
|
});
|
|
308
308
|
|
|
309
|
+
test(`${capitalizeFirstLetter(provider)}: should handle parallel tool usage (web search + calculator)`, async () => {
|
|
310
|
+
const llmConfig = getLLMConfig(provider);
|
|
311
|
+
const customHandlers = setupCustomHandlers();
|
|
312
|
+
|
|
313
|
+
run = await Run.create<t.IState>({
|
|
314
|
+
runId: 'test-parallel-tools',
|
|
315
|
+
graphConfig: {
|
|
316
|
+
type: 'standard',
|
|
317
|
+
llmConfig,
|
|
318
|
+
tools: [
|
|
319
|
+
{
|
|
320
|
+
type: 'web_search_20250305',
|
|
321
|
+
name: 'web_search',
|
|
322
|
+
max_uses: 5,
|
|
323
|
+
},
|
|
324
|
+
new Calculator(),
|
|
325
|
+
],
|
|
326
|
+
instructions: 'You are a helpful AI assistant.',
|
|
327
|
+
},
|
|
328
|
+
returnContent: true,
|
|
329
|
+
customHandlers,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Use the same query as the edge case script to test actual parallel tool usage
|
|
333
|
+
const userMessage =
|
|
334
|
+
'Can you search the web for the current population of Tokyo, and also calculate what 15% of that population would be? Do both at the same time.';
|
|
335
|
+
conversationHistory = [];
|
|
336
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
337
|
+
|
|
338
|
+
const inputs = {
|
|
339
|
+
messages: conversationHistory,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// This should complete without errors despite using both server tools and regular tools in parallel
|
|
343
|
+
const finalContentParts = await run.processStream(inputs, config);
|
|
344
|
+
expect(finalContentParts).toBeDefined();
|
|
345
|
+
|
|
346
|
+
const finalMessages = run.getRunMessages();
|
|
347
|
+
expect(finalMessages).toBeDefined();
|
|
348
|
+
expect(finalMessages?.length).toBeGreaterThan(0);
|
|
349
|
+
|
|
350
|
+
const hasWebSearch = contentParts.some(
|
|
351
|
+
(part) =>
|
|
352
|
+
!!(
|
|
353
|
+
part.type === 'tool_call' &&
|
|
354
|
+
part.tool_call?.name === 'web_search' &&
|
|
355
|
+
part.tool_call?.id?.startsWith('srvtoolu_') === true
|
|
356
|
+
)
|
|
357
|
+
);
|
|
358
|
+
const hasCalculator = contentParts.some(
|
|
359
|
+
(part) =>
|
|
360
|
+
!!(
|
|
361
|
+
part.type === 'tool_call' &&
|
|
362
|
+
part.tool_call?.name === 'calculator' &&
|
|
363
|
+
part.tool_call?.id?.startsWith('toolu_') === true
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Both tools should have been used for this query
|
|
368
|
+
expect(hasWebSearch).toBe(true);
|
|
369
|
+
expect(hasCalculator).toBe(true);
|
|
370
|
+
|
|
371
|
+
console.log(
|
|
372
|
+
`${capitalizeFirstLetter(provider)} parallel tools test: web_search (server tool) + calculator (regular tool) both used successfully`
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
309
376
|
test('should handle errors appropriately', async () => {
|
|
310
377
|
// Test error scenarios
|
|
311
378
|
await expect(async () => {
|
package/src/stream.ts
CHANGED
|
@@ -155,7 +155,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
155
155
|
chunk.tool_calls.length > 0 &&
|
|
156
156
|
chunk.tool_calls.every(
|
|
157
157
|
(tc) =>
|
|
158
|
-
tc.id != null &&
|
|
158
|
+
tc.id != null &&
|
|
159
|
+
tc.id !== '' &&
|
|
160
|
+
(tc as Partial<ToolCall>).name != null &&
|
|
161
|
+
tc.name !== ''
|
|
159
162
|
)
|
|
160
163
|
) {
|
|
161
164
|
hasToolCalls = true;
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -201,7 +201,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
201
201
|
|
|
202
202
|
outputs = await Promise.all(
|
|
203
203
|
aiMessage.tool_calls
|
|
204
|
-
?.filter((call) =>
|
|
204
|
+
?.filter((call) => {
|
|
205
|
+
/**
|
|
206
|
+
* Filter out:
|
|
207
|
+
* 1. Already processed tool calls (present in toolMessageIds)
|
|
208
|
+
* 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
|
|
209
|
+
* which are executed by the provider's API and don't require invocation
|
|
210
|
+
*/
|
|
211
|
+
return (
|
|
212
|
+
(call.id == null || !toolMessageIds.has(call.id)) &&
|
|
213
|
+
!(call.id?.startsWith('srvtoolu_') ?? false)
|
|
214
|
+
);
|
|
215
|
+
})
|
|
205
216
|
.map((call) => this.runTool(call, config)) ?? []
|
|
206
217
|
);
|
|
207
218
|
}
|