@librechat/agents 2.2.5 → 2.2.6

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.
@@ -0,0 +1,349 @@
1
+ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
2
+ import type { TPayload } from '@/types';
3
+ import { formatAgentMessages } from './format';
4
+ import { ContentTypes } from '@/common';
5
+
6
+ describe('formatAgentMessages with tools parameter', () => {
7
+ it('should process messages normally when tools is not provided', () => {
8
+ const payload: TPayload = [
9
+ { role: 'user', content: 'Hello' },
10
+ {
11
+ role: 'assistant',
12
+ content: [
13
+ {
14
+ type: ContentTypes.TEXT,
15
+ [ContentTypes.TEXT]: 'Let me check that for you.',
16
+ tool_call_ids: ['123'],
17
+ },
18
+ {
19
+ type: ContentTypes.TOOL_CALL,
20
+ tool_call: {
21
+ id: '123',
22
+ name: 'search',
23
+ args: '{"query":"weather"}',
24
+ output: 'The weather is sunny.',
25
+ },
26
+ },
27
+ ],
28
+ },
29
+ ];
30
+
31
+ const result = formatAgentMessages(payload);
32
+
33
+ expect(result.messages).toHaveLength(3);
34
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
35
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
36
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage);
37
+ expect((result.messages[1] as AIMessage).tool_calls).toHaveLength(1);
38
+ expect((result.messages[2] as ToolMessage).tool_call_id).toBe('123');
39
+ });
40
+
41
+ it('should treat an empty tools set the same as disallowing all tools', () => {
42
+ const payload: TPayload = [
43
+ { role: 'user', content: 'What\'s the weather?' },
44
+ {
45
+ role: 'assistant',
46
+ content: [
47
+ {
48
+ type: ContentTypes.TEXT,
49
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
50
+ tool_call_ids: ['weather_1'],
51
+ },
52
+ {
53
+ type: ContentTypes.TOOL_CALL,
54
+ tool_call: {
55
+ id: 'weather_1',
56
+ name: 'check_weather',
57
+ args: '{"location":"New York"}',
58
+ output: 'Sunny, 75°F',
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ ];
64
+
65
+ // Provide an empty set of allowed tools
66
+ const allowedTools = new Set<string>();
67
+
68
+ const result = formatAgentMessages(payload, undefined, allowedTools);
69
+
70
+ // Should convert to a single AIMessage with string content
71
+ expect(result.messages).toHaveLength(2);
72
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
73
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
74
+
75
+ // The content should be a string representation of both messages
76
+ expect(typeof result.messages[1].content).toBe('string');
77
+ expect(result.messages[1].content).toEqual('AI: Let me check the weather for you.\nTool: check_weather, Sunny, 75°F');
78
+ });
79
+
80
+ it('should convert tool messages to string when tool is not in the allowed set', () => {
81
+ const payload: TPayload = [
82
+ { role: 'user', content: 'What\'s the weather?' },
83
+ {
84
+ role: 'assistant',
85
+ content: [
86
+ {
87
+ type: ContentTypes.TEXT,
88
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
89
+ tool_call_ids: ['weather_1'],
90
+ },
91
+ {
92
+ type: ContentTypes.TOOL_CALL,
93
+ tool_call: {
94
+ id: 'weather_1',
95
+ name: 'check_weather',
96
+ args: '{"location":"New York"}',
97
+ output: 'Sunny, 75°F',
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ ];
103
+
104
+ // Provide a set of allowed tools that doesn't include 'check_weather'
105
+ const allowedTools = new Set(['search', 'calculator']);
106
+
107
+ const result = formatAgentMessages(payload, undefined, allowedTools);
108
+
109
+ // Should convert to a single AIMessage with string content
110
+ expect(result.messages).toHaveLength(2);
111
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
112
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
113
+
114
+ // The content should be a string representation of both messages
115
+ expect(typeof result.messages[1].content).toBe('string');
116
+ expect(result.messages[1].content).toEqual('AI: Let me check the weather for you.\nTool: check_weather, Sunny, 75°F');
117
+ });
118
+
119
+ it('should not convert tool messages when tool is in the allowed set', () => {
120
+ const payload: TPayload = [
121
+ { role: 'user', content: 'What\'s the weather?' },
122
+ {
123
+ role: 'assistant',
124
+ content: [
125
+ {
126
+ type: ContentTypes.TEXT,
127
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
128
+ tool_call_ids: ['weather_1'],
129
+ },
130
+ {
131
+ type: ContentTypes.TOOL_CALL,
132
+ tool_call: {
133
+ id: 'weather_1',
134
+ name: 'check_weather',
135
+ args: '{"location":"New York"}',
136
+ output: 'Sunny, 75°F',
137
+ },
138
+ },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ // Provide a set of allowed tools that includes 'check_weather'
144
+ const allowedTools = new Set(['check_weather', 'search']);
145
+
146
+ const result = formatAgentMessages(payload, undefined, allowedTools);
147
+
148
+ // Should keep the original structure
149
+ expect(result.messages).toHaveLength(3);
150
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
151
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
152
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage);
153
+ });
154
+
155
+ it('should handle multiple tool calls with mixed allowed/disallowed tools', () => {
156
+ const payload: TPayload = [
157
+ { role: 'user', content: 'Tell me about the weather and calculate something' },
158
+ {
159
+ role: 'assistant',
160
+ content: [
161
+ {
162
+ type: ContentTypes.TEXT,
163
+ [ContentTypes.TEXT]: 'Let me check the weather first.',
164
+ tool_call_ids: ['weather_1'],
165
+ },
166
+ {
167
+ type: ContentTypes.TOOL_CALL,
168
+ tool_call: {
169
+ id: 'weather_1',
170
+ name: 'check_weather',
171
+ args: '{"location":"New York"}',
172
+ output: 'Sunny, 75°F',
173
+ },
174
+ },
175
+ {
176
+ type: ContentTypes.TEXT,
177
+ [ContentTypes.TEXT]: 'Now let me calculate something for you.',
178
+ tool_call_ids: ['calc_1'],
179
+ },
180
+ {
181
+ type: ContentTypes.TOOL_CALL,
182
+ tool_call: {
183
+ id: 'calc_1',
184
+ name: 'calculator',
185
+ args: '{"expression":"1+1"}',
186
+ output: '2',
187
+ },
188
+ },
189
+ ],
190
+ },
191
+ ];
192
+
193
+ // Allow calculator but not check_weather
194
+ const allowedTools = new Set(['calculator', 'search']);
195
+
196
+ const result = formatAgentMessages(payload, undefined, allowedTools);
197
+
198
+ // Should convert the entire sequence to a single AIMessage
199
+ expect(result.messages).toHaveLength(2);
200
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
201
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
202
+
203
+ // The content should include all parts
204
+ expect(typeof result.messages[1].content).toBe('string');
205
+ expect(result.messages[1].content).toContain('Let me check the weather first.');
206
+ expect(result.messages[1].content).toContain('Sunny, 75°F');
207
+ expect(result.messages[1].content).toContain('Now let me calculate something for you.');
208
+ expect(result.messages[1].content).toContain('2');
209
+ });
210
+
211
+ it('should update indexTokenCountMap correctly when converting tool messages', () => {
212
+ const payload: TPayload = [
213
+ { role: 'user', content: 'What\'s the weather?' },
214
+ {
215
+ role: 'assistant',
216
+ content: [
217
+ {
218
+ type: ContentTypes.TEXT,
219
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
220
+ tool_call_ids: ['weather_1'],
221
+ },
222
+ {
223
+ type: ContentTypes.TOOL_CALL,
224
+ tool_call: {
225
+ id: 'weather_1',
226
+ name: 'check_weather',
227
+ args: '{"location":"New York"}',
228
+ output: 'Sunny, 75°F',
229
+ },
230
+ },
231
+ ],
232
+ },
233
+ ];
234
+
235
+ const indexTokenCountMap = {
236
+ 0: 10, // 10 tokens for user message
237
+ 1: 40, // 40 tokens for assistant message with tool call
238
+ };
239
+
240
+ // Provide a set of allowed tools that doesn't include 'check_weather'
241
+ const allowedTools = new Set(['search', 'calculator']);
242
+
243
+ const result = formatAgentMessages(payload, indexTokenCountMap, allowedTools);
244
+
245
+ // Should have 2 messages and 2 entries in the token count map
246
+ expect(result.messages).toHaveLength(2);
247
+ expect(Object.keys(result.indexTokenCountMap || {}).length).toBe(2);
248
+
249
+ // User message token count should be unchanged
250
+ expect(result.indexTokenCountMap?.[0]).toBe(10);
251
+
252
+ // All assistant message tokens should be assigned to the single AIMessage
253
+ expect(result.indexTokenCountMap?.[1]).toBe(40);
254
+ });
255
+
256
+ it('should handle complex sequences with multiple tool calls', () => {
257
+ const payload: TPayload = [
258
+ { role: 'user', content: 'Help me with a complex task' },
259
+ {
260
+ role: 'assistant',
261
+ content: [
262
+ {
263
+ type: ContentTypes.TEXT,
264
+ [ContentTypes.TEXT]: 'I\'ll search for information first.',
265
+ tool_call_ids: ['search_1'],
266
+ },
267
+ {
268
+ type: ContentTypes.TOOL_CALL,
269
+ tool_call: {
270
+ id: 'search_1',
271
+ name: 'search',
272
+ args: '{"query":"complex task"}',
273
+ output: 'Found information about complex tasks.',
274
+ },
275
+ },
276
+ ],
277
+ },
278
+ {
279
+ role: 'assistant',
280
+ content: [
281
+ {
282
+ type: ContentTypes.TEXT,
283
+ [ContentTypes.TEXT]: 'Now I\'ll check the weather.',
284
+ tool_call_ids: ['weather_1'],
285
+ },
286
+ {
287
+ type: ContentTypes.TOOL_CALL,
288
+ tool_call: {
289
+ id: 'weather_1',
290
+ name: 'check_weather',
291
+ args: '{"location":"New York"}',
292
+ output: 'Sunny, 75°F',
293
+ },
294
+ },
295
+ ],
296
+ },
297
+ {
298
+ role: 'assistant',
299
+ content: [
300
+ {
301
+ type: ContentTypes.TEXT,
302
+ [ContentTypes.TEXT]: 'Finally, I\'ll calculate something.',
303
+ tool_call_ids: ['calc_1'],
304
+ },
305
+ {
306
+ type: ContentTypes.TOOL_CALL,
307
+ tool_call: {
308
+ id: 'calc_1',
309
+ name: 'calculator',
310
+ args: '{"expression":"1+1"}',
311
+ output: '2',
312
+ },
313
+ },
314
+ ],
315
+ },
316
+ { role: 'assistant', content: 'Here\'s your answer based on all that information.' },
317
+ ];
318
+
319
+ // Allow search and calculator but not check_weather
320
+ const allowedTools = new Set(['search', 'calculator']);
321
+
322
+ const result = formatAgentMessages(payload, undefined, allowedTools);
323
+
324
+ // Should have the user message, search tool sequence (2 messages),
325
+ // a combined message for weather and calculator (since one has an invalid tool),
326
+ // and final message
327
+ expect(result.messages).toHaveLength(5);
328
+
329
+ // Check the types of messages
330
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
331
+ expect(result.messages[1]).toBeInstanceOf(AIMessage); // Search message
332
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage); // Search tool response
333
+ expect(result.messages[3]).toBeInstanceOf(AIMessage); // Converted weather+calculator message
334
+ expect(result.messages[4]).toBeInstanceOf(AIMessage); // Final message
335
+
336
+ // Check that the combined message was converted to a string
337
+ expect(typeof result.messages[3].content).toBe('string');
338
+
339
+ // The format might vary based on the getBufferString implementation
340
+ // but we should check that all the key information is present
341
+ const content = result.messages[3].content as string;
342
+ expect(content).toContain('Now I\'ll check the weather');
343
+ expect(content).toContain('Sunny');
344
+ expect(content).toContain('75');
345
+ expect(content).toContain('Finally');
346
+ expect(content).toContain('calculate');
347
+ expect(content).toContain('2');
348
+ });
349
+ });
@@ -240,8 +240,35 @@ export type BedrockReasoningContentText = {
240
240
  reasoningText: { text?: string; signature?: string; }
241
241
  };
242
242
 
243
+ /**
244
+ * A call to a tool.
245
+ */
246
+ export type ToolCallPart = {
247
+ /** Type ("tool_call") according to Assistants Tool Call Structure */
248
+ type: ContentTypes.TOOL_CALL;
249
+ /** The name of the tool to be called */
250
+ name: string;
251
+ /** The arguments to the tool call */
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ args?: string | Record<string, any>;
254
+
255
+ /** If provided, an identifier associated with the tool call */
256
+ id?: string;
257
+ /** If provided, the output of the tool call */
258
+ output?: string;
259
+ /** Auth URL */
260
+ auth?: string;
261
+ /** Expiration time */
262
+ expires_at?: number;
263
+ };
264
+
265
+ export type ToolCallContent = {
266
+ type: ContentTypes.TOOL_CALL;
267
+ tool_call?: ToolCallPart;
268
+ };
269
+
243
270
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
- export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
271
+ export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ToolCallContent | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
245
272
  type?: 'text' | 'image_url' | 'think' | 'thinking' | string;
246
273
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
274
  }) | (Record<string, any> & {
@@ -249,7 +276,14 @@ export type MessageContentComplex = (ThinkingContentText | AgentUpdate | Reasoni
249
276
  })) & {
250
277
  tool_call_ids?: string[];
251
278
  };
252
- // #new
279
+
280
+ export interface TMessage {
281
+ role?: string;
282
+ content?: MessageContentComplex[] | string;
283
+ [key: string]: any;
284
+ }
285
+
286
+ export type TPayload = Array<Partial<TMessage>>;
253
287
 
254
288
  export type CustomChunk = Partial<OpenAITypes.ChatCompletionChunk> & {
255
289
  choices?: Partial<Array<Partial<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice> & {
@@ -42,11 +42,6 @@ export type ToolEndEvent = {
42
42
  index: number;
43
43
  };
44
44
 
45
- export type ToolCallContent = {
46
- type: ContentTypes.TOOL_CALL;
47
- tool_call: ToolCall;
48
- };
49
-
50
45
  export type CodeEnvFile = {
51
46
  id: string;
52
47
  name: string;