@librechat/agents 3.0.13 → 3.0.15
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/graphs/Graph.cjs +6 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +24 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +6 -2
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +6 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +24 -0
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +6 -2
- package/dist/esm/messages/format.mjs.map +1 -1
- package/package.json +2 -2
- package/src/graphs/Graph.ts +10 -0
- package/src/messages/cache.test.ts +16 -3
- package/src/messages/cache.ts +31 -0
- package/src/messages/format.ts +12 -6
- package/src/messages/formatAgentMessages.test.ts +167 -0
- package/src/scripts/search.ts +5 -1
- package/src/scripts/tools.ts +11 -10
package/src/messages/cache.ts
CHANGED
|
@@ -83,8 +83,21 @@ export function addBedrockCacheControl<
|
|
|
83
83
|
i--
|
|
84
84
|
) {
|
|
85
85
|
const message = updatedMessages[i];
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
'getType' in message &&
|
|
89
|
+
typeof message.getType === 'function' &&
|
|
90
|
+
message.getType() === 'tool'
|
|
91
|
+
) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
const content = message.content;
|
|
87
96
|
|
|
97
|
+
if (typeof content === 'string' && content === '') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
if (typeof content === 'string') {
|
|
89
102
|
message.content = [
|
|
90
103
|
{ type: ContentTypes.TEXT, text: content },
|
|
@@ -95,11 +108,29 @@ export function addBedrockCacheControl<
|
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
if (Array.isArray(content)) {
|
|
111
|
+
let hasCacheableContent = false;
|
|
112
|
+
for (const block of content) {
|
|
113
|
+
if (block.type === ContentTypes.TEXT) {
|
|
114
|
+
if (typeof block.text === 'string' && block.text !== '') {
|
|
115
|
+
hasCacheableContent = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!hasCacheableContent) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
let inserted = false;
|
|
99
126
|
for (let j = content.length - 1; j >= 0; j--) {
|
|
100
127
|
const block = content[j] as MessageContentComplex;
|
|
101
128
|
const type = (block as { type?: string }).type;
|
|
102
129
|
if (type === ContentTypes.TEXT || type === 'text') {
|
|
130
|
+
const text = (block as { text?: string }).text;
|
|
131
|
+
if (text === '' || text === undefined) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
103
134
|
content.splice(j + 1, 0, {
|
|
104
135
|
cachePoint: { type: 'default' },
|
|
105
136
|
} as MessageContentComplex);
|
package/src/messages/format.ts
CHANGED
|
@@ -310,18 +310,24 @@ function formatAssistantMessage(
|
|
|
310
310
|
});
|
|
311
311
|
formattedMessages.push(lastAIMessage);
|
|
312
312
|
} else if (part.type === ContentTypes.TOOL_CALL) {
|
|
313
|
-
if (!lastAIMessage) {
|
|
314
|
-
// "Heal" the payload by creating an AIMessage to precede the tool call
|
|
315
|
-
lastAIMessage = new AIMessage({ content: '' });
|
|
316
|
-
formattedMessages.push(lastAIMessage);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
313
|
// Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
|
|
320
314
|
const {
|
|
321
315
|
output,
|
|
322
316
|
args: _args,
|
|
323
317
|
..._tool_call
|
|
324
318
|
} = part.tool_call as ToolCallPart;
|
|
319
|
+
|
|
320
|
+
// Skip invalid tool calls that have no name AND no output
|
|
321
|
+
if (!_tool_call.name && (output == null || output === '')) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!lastAIMessage) {
|
|
326
|
+
// "Heal" the payload by creating an AIMessage to precede the tool call
|
|
327
|
+
lastAIMessage = new AIMessage({ content: '' });
|
|
328
|
+
formattedMessages.push(lastAIMessage);
|
|
329
|
+
}
|
|
330
|
+
|
|
325
331
|
const tool_call: ToolCallPart = _tool_call;
|
|
326
332
|
// TODO: investigate; args as dictionary may need to be providers-or-tool-specific
|
|
327
333
|
let args: any = _args;
|
|
@@ -914,4 +914,171 @@ describe('formatAgentMessages', () => {
|
|
|
914
914
|
|
|
915
915
|
expect(totalTokens).toBe(10);
|
|
916
916
|
});
|
|
917
|
+
|
|
918
|
+
it('should skip invalid tool calls with no name AND no output', () => {
|
|
919
|
+
const payload = [
|
|
920
|
+
{
|
|
921
|
+
role: 'assistant',
|
|
922
|
+
content: [
|
|
923
|
+
{
|
|
924
|
+
type: ContentTypes.TEXT,
|
|
925
|
+
[ContentTypes.TEXT]: 'Let me help you with that.',
|
|
926
|
+
tool_call_ids: ['valid_tool_1'],
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
type: ContentTypes.TOOL_CALL,
|
|
930
|
+
tool_call: {
|
|
931
|
+
id: 'invalid_tool_1',
|
|
932
|
+
name: '',
|
|
933
|
+
args: '{"query":"test"}',
|
|
934
|
+
output: '',
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
type: ContentTypes.TOOL_CALL,
|
|
939
|
+
tool_call: {
|
|
940
|
+
id: 'valid_tool_1',
|
|
941
|
+
name: 'search',
|
|
942
|
+
args: '{"query":"weather"}',
|
|
943
|
+
output: 'The weather is sunny.',
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
];
|
|
949
|
+
|
|
950
|
+
const result = formatAgentMessages(payload);
|
|
951
|
+
|
|
952
|
+
// Should have 2 messages: AIMessage and ToolMessage (invalid tool call is skipped)
|
|
953
|
+
expect(result.messages).toHaveLength(2);
|
|
954
|
+
expect(result.messages[0]).toBeInstanceOf(AIMessage);
|
|
955
|
+
expect(result.messages[1]).toBeInstanceOf(ToolMessage);
|
|
956
|
+
|
|
957
|
+
// The AIMessage should only have 1 tool call (the valid one)
|
|
958
|
+
expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
|
|
959
|
+
expect((result.messages[0] as AIMessage).tool_calls?.[0].name).toBe(
|
|
960
|
+
'search'
|
|
961
|
+
);
|
|
962
|
+
expect((result.messages[0] as AIMessage).tool_calls?.[0].id).toBe(
|
|
963
|
+
'valid_tool_1'
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
// The ToolMessage should be for the valid tool call
|
|
967
|
+
expect((result.messages[1] as ToolMessage).tool_call_id).toBe(
|
|
968
|
+
'valid_tool_1'
|
|
969
|
+
);
|
|
970
|
+
expect(result.messages[1].name).toBe('search');
|
|
971
|
+
expect(result.messages[1].content).toBe('The weather is sunny.');
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('should skip tool calls with no name AND null output', () => {
|
|
975
|
+
const payload = [
|
|
976
|
+
{
|
|
977
|
+
role: 'assistant',
|
|
978
|
+
content: [
|
|
979
|
+
{
|
|
980
|
+
type: ContentTypes.TOOL_CALL,
|
|
981
|
+
tool_call: {
|
|
982
|
+
id: 'invalid_tool_1',
|
|
983
|
+
name: '',
|
|
984
|
+
args: '{"query":"test"}',
|
|
985
|
+
output: null,
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
type: ContentTypes.TEXT,
|
|
990
|
+
[ContentTypes.TEXT]: 'Here is the information.',
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
},
|
|
994
|
+
];
|
|
995
|
+
|
|
996
|
+
const result = formatAgentMessages(payload);
|
|
997
|
+
|
|
998
|
+
// Should have 1 message: AIMessage (invalid tool call is skipped)
|
|
999
|
+
expect(result.messages).toHaveLength(1);
|
|
1000
|
+
expect(result.messages[0]).toBeInstanceOf(AIMessage);
|
|
1001
|
+
|
|
1002
|
+
// The AIMessage should have no tool calls or an empty array
|
|
1003
|
+
const toolCalls = (result.messages[0] as AIMessage).tool_calls;
|
|
1004
|
+
expect(toolCalls === undefined || toolCalls.length === 0).toBe(true);
|
|
1005
|
+
expect(result.messages[0].content).toStrictEqual([
|
|
1006
|
+
{
|
|
1007
|
+
type: ContentTypes.TEXT,
|
|
1008
|
+
[ContentTypes.TEXT]: 'Here is the information.',
|
|
1009
|
+
},
|
|
1010
|
+
]);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('should NOT skip tool calls with no name but valid output', () => {
|
|
1014
|
+
const payload = [
|
|
1015
|
+
{
|
|
1016
|
+
role: 'assistant',
|
|
1017
|
+
content: [
|
|
1018
|
+
{
|
|
1019
|
+
type: ContentTypes.TOOL_CALL,
|
|
1020
|
+
tool_call: {
|
|
1021
|
+
id: 'tool_1',
|
|
1022
|
+
name: '',
|
|
1023
|
+
args: '{"query":"test"}',
|
|
1024
|
+
output: 'Valid output despite missing name',
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
},
|
|
1029
|
+
];
|
|
1030
|
+
|
|
1031
|
+
const result = formatAgentMessages(payload);
|
|
1032
|
+
|
|
1033
|
+
// Should have 2 messages: AIMessage and ToolMessage
|
|
1034
|
+
expect(result.messages).toHaveLength(2);
|
|
1035
|
+
expect(result.messages[0]).toBeInstanceOf(AIMessage);
|
|
1036
|
+
expect(result.messages[1]).toBeInstanceOf(ToolMessage);
|
|
1037
|
+
|
|
1038
|
+
// The AIMessage should have 1 tool call
|
|
1039
|
+
expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
|
|
1040
|
+
|
|
1041
|
+
// The ToolMessage should have the output
|
|
1042
|
+
expect((result.messages[1] as ToolMessage).tool_call_id).toBe('tool_1');
|
|
1043
|
+
expect(result.messages[1].content).toBe(
|
|
1044
|
+
'Valid output despite missing name'
|
|
1045
|
+
);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('should NOT skip tool calls with valid name but no output', () => {
|
|
1049
|
+
const payload = [
|
|
1050
|
+
{
|
|
1051
|
+
role: 'assistant',
|
|
1052
|
+
content: [
|
|
1053
|
+
{
|
|
1054
|
+
type: ContentTypes.TOOL_CALL,
|
|
1055
|
+
tool_call: {
|
|
1056
|
+
id: 'tool_1',
|
|
1057
|
+
name: 'search',
|
|
1058
|
+
args: '{"query":"test"}',
|
|
1059
|
+
output: '',
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
},
|
|
1064
|
+
];
|
|
1065
|
+
|
|
1066
|
+
const result = formatAgentMessages(payload);
|
|
1067
|
+
|
|
1068
|
+
// Should have 2 messages: AIMessage and ToolMessage
|
|
1069
|
+
expect(result.messages).toHaveLength(2);
|
|
1070
|
+
expect(result.messages[0]).toBeInstanceOf(AIMessage);
|
|
1071
|
+
expect(result.messages[1]).toBeInstanceOf(ToolMessage);
|
|
1072
|
+
|
|
1073
|
+
// The AIMessage should have 1 tool call
|
|
1074
|
+
expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
|
|
1075
|
+
expect((result.messages[0] as AIMessage).tool_calls?.[0].name).toBe(
|
|
1076
|
+
'search'
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
// The ToolMessage should have empty content
|
|
1080
|
+
expect((result.messages[1] as ToolMessage).tool_call_id).toBe('tool_1');
|
|
1081
|
+
expect(result.messages[1].name).toBe('search');
|
|
1082
|
+
expect(result.messages[1].content).toBe('');
|
|
1083
|
+
});
|
|
917
1084
|
});
|
package/src/scripts/search.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { createSearchTool } from '@/tools/search';
|
|
|
10
10
|
|
|
11
11
|
import { getArgs } from '@/scripts/args';
|
|
12
12
|
import { Run } from '@/run';
|
|
13
|
-
import { GraphEvents, Callback } from '@/common';
|
|
13
|
+
import { GraphEvents, Callback, Providers } from '@/common';
|
|
14
14
|
import { getLLMConfig } from '@/utils/llmConfig';
|
|
15
15
|
|
|
16
16
|
const conversationHistory: BaseMessage[] = [];
|
|
@@ -72,6 +72,10 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
72
72
|
|
|
73
73
|
const llmConfig = getLLMConfig(provider);
|
|
74
74
|
|
|
75
|
+
if (llmConfig.provider === Providers.BEDROCK) {
|
|
76
|
+
(llmConfig as t.BedrockAnthropicInput).promptCache = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
75
79
|
const run = await Run.create<t.IState>({
|
|
76
80
|
runId: 'test-run-id',
|
|
77
81
|
graphConfig: {
|
package/src/scripts/tools.ts
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
// src/scripts/cli.ts
|
|
3
3
|
import { config } from 'dotenv';
|
|
4
4
|
config();
|
|
5
|
+
|
|
5
6
|
import { HumanMessage, BaseMessage } from '@langchain/core/messages';
|
|
6
7
|
import type * as t from '@/types';
|
|
7
8
|
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
8
9
|
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
9
|
-
|
|
10
|
+
import { GraphEvents, Providers } from '@/common';
|
|
11
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
12
|
+
import { Calculator } from '@/tools/Calculator';
|
|
10
13
|
import { getArgs } from '@/scripts/args';
|
|
11
14
|
import { Run } from '@/run';
|
|
12
|
-
import { GraphEvents, Callback } from '@/common';
|
|
13
|
-
import { getLLMConfig } from '@/utils/llmConfig';
|
|
14
15
|
|
|
15
16
|
const conversationHistory: BaseMessage[] = [];
|
|
16
17
|
async function testStandardStreaming(): Promise<void> {
|
|
@@ -89,12 +90,16 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
89
90
|
|
|
90
91
|
const llmConfig = getLLMConfig(provider);
|
|
91
92
|
|
|
93
|
+
if (llmConfig.provider === Providers.BEDROCK) {
|
|
94
|
+
(llmConfig as t.BedrockAnthropicInput).promptCache = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
const run = await Run.create<t.IState>({
|
|
93
98
|
runId: 'test-run-id',
|
|
94
99
|
graphConfig: {
|
|
95
100
|
type: 'standard',
|
|
96
101
|
llmConfig,
|
|
97
|
-
tools: [],
|
|
102
|
+
tools: [new Calculator()],
|
|
98
103
|
instructions:
|
|
99
104
|
'You are a friendly AI assistant. Always address the user by their name.',
|
|
100
105
|
additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
|
|
@@ -114,13 +119,9 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
114
119
|
version: 'v2' as const,
|
|
115
120
|
};
|
|
116
121
|
|
|
117
|
-
console.log('Test 1:
|
|
122
|
+
console.log('Test 1: Calculation query');
|
|
118
123
|
|
|
119
|
-
const userMessage = `
|
|
120
|
-
Make a search for the weather in ${location} today, which is ${currentDate}.
|
|
121
|
-
Make sure to always refer to me by name, which is ${userName}.
|
|
122
|
-
After giving me a thorough summary, tell me a joke about the weather forecast we went over.
|
|
123
|
-
`;
|
|
124
|
+
const userMessage = `What is 1123123 + 123123 / 20348?`;
|
|
124
125
|
|
|
125
126
|
conversationHistory.push(new HumanMessage(userMessage));
|
|
126
127
|
|