@librechat/agents 2.2.4 → 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.
- package/dist/cjs/graphs/Graph.cjs +1 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +161 -80
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +9 -2
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +1 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +162 -81
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +9 -2
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +6 -15
- package/dist/types/messages/prune.d.ts +1 -0
- package/dist/types/types/stream.d.ts +30 -1
- package/dist/types/types/tools.d.ts +1 -5
- package/package.json +1 -1
- package/src/graphs/Graph.ts +1 -0
- package/src/messages/format.ts +180 -111
- package/src/messages/formatAgentMessages.test.ts +2 -1
- package/src/messages/formatAgentMessages.tools.test.ts +349 -0
- package/src/messages/prune.ts +14 -2
- package/src/specs/prune.test.ts +121 -0
- package/src/types/stream.ts +36 -2
- package/src/types/tools.ts +0 -5
|
@@ -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
|
+
});
|
package/src/messages/prune.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type PruneMessagesFactoryParams = {
|
|
|
9
9
|
export type PruneMessagesParams = {
|
|
10
10
|
messages: BaseMessage[];
|
|
11
11
|
usageMetadata?: Partial<UsageMetadata>;
|
|
12
|
+
startOnMessageType?: ReturnType<BaseMessage['getType']>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -43,10 +44,12 @@ function getMessagesWithinTokenLimit({
|
|
|
43
44
|
messages: _messages,
|
|
44
45
|
maxContextTokens,
|
|
45
46
|
indexTokenCountMap,
|
|
47
|
+
startOnMessageType,
|
|
46
48
|
}: {
|
|
47
49
|
messages: BaseMessage[];
|
|
48
50
|
maxContextTokens: number;
|
|
49
51
|
indexTokenCountMap: Record<string, number>;
|
|
52
|
+
startOnMessageType?: string;
|
|
50
53
|
}): {
|
|
51
54
|
context: BaseMessage[];
|
|
52
55
|
remainingContextTokens: number;
|
|
@@ -61,7 +64,7 @@ function getMessagesWithinTokenLimit({
|
|
|
61
64
|
const instructionsTokenCount = instructions != null ? indexTokenCountMap[0] : 0;
|
|
62
65
|
let remainingContextTokens = maxContextTokens - instructionsTokenCount;
|
|
63
66
|
const messages = [..._messages];
|
|
64
|
-
|
|
67
|
+
let context: BaseMessage[] = [];
|
|
65
68
|
|
|
66
69
|
if (currentTokenCount < remainingContextTokens) {
|
|
67
70
|
let currentIndex = messages.length;
|
|
@@ -83,6 +86,14 @@ function getMessagesWithinTokenLimit({
|
|
|
83
86
|
break;
|
|
84
87
|
}
|
|
85
88
|
}
|
|
89
|
+
|
|
90
|
+
if (startOnMessageType && context.length > 0) {
|
|
91
|
+
const requiredTypeIndex = context.findIndex(msg => msg.getType() === startOnMessageType);
|
|
92
|
+
|
|
93
|
+
if (requiredTypeIndex > 0) {
|
|
94
|
+
context = context.slice(requiredTypeIndex);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
if (instructions && _messages.length > 0) {
|
|
@@ -160,8 +171,9 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
160
171
|
maxContextTokens: factoryParams.maxTokens,
|
|
161
172
|
messages: params.messages,
|
|
162
173
|
indexTokenCountMap,
|
|
174
|
+
startOnMessageType: params.startOnMessageType,
|
|
163
175
|
});
|
|
164
176
|
|
|
165
177
|
return { context, indexTokenCountMap };
|
|
166
178
|
}
|
|
167
|
-
}
|
|
179
|
+
}
|
package/src/specs/prune.test.ts
CHANGED
|
@@ -65,10 +65,12 @@ function getMessagesWithinTokenLimit({
|
|
|
65
65
|
messages: _messages,
|
|
66
66
|
maxContextTokens,
|
|
67
67
|
indexTokenCountMap,
|
|
68
|
+
startOnMessageType,
|
|
68
69
|
}: {
|
|
69
70
|
messages: BaseMessage[];
|
|
70
71
|
maxContextTokens: number;
|
|
71
72
|
indexTokenCountMap: Record<string, number>;
|
|
73
|
+
startOnMessageType?: string;
|
|
72
74
|
}): {
|
|
73
75
|
context: BaseMessage[];
|
|
74
76
|
remainingContextTokens: number;
|
|
@@ -105,6 +107,18 @@ function getMessagesWithinTokenLimit({
|
|
|
105
107
|
break;
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
|
|
111
|
+
// If startOnMessageType is specified, discard messages until we find one of the required type
|
|
112
|
+
if (startOnMessageType && context.length > 0) {
|
|
113
|
+
const requiredTypeIndex = context.findIndex(msg => msg.getType() === startOnMessageType);
|
|
114
|
+
|
|
115
|
+
if (requiredTypeIndex > 0) {
|
|
116
|
+
// If we found a message of the required type, discard all messages before it
|
|
117
|
+
const remainingMessages = context.slice(requiredTypeIndex);
|
|
118
|
+
context.length = 0; // Clear the array
|
|
119
|
+
context.push(...remainingMessages);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
if (instructions && _messages.length > 0) {
|
|
@@ -262,6 +276,73 @@ describe('Prune Messages Tests', () => {
|
|
|
262
276
|
|
|
263
277
|
expect(result.messagesToRefine.length).toBe(2);
|
|
264
278
|
});
|
|
279
|
+
|
|
280
|
+
it('should start context with a specific message type when startOnMessageType is specified', () => {
|
|
281
|
+
const messages = [
|
|
282
|
+
new SystemMessage('System instruction'),
|
|
283
|
+
new AIMessage('AI message 1'),
|
|
284
|
+
new HumanMessage('Human message 1'),
|
|
285
|
+
new AIMessage('AI message 2'),
|
|
286
|
+
new HumanMessage('Human message 2')
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const indexTokenCountMap = {
|
|
290
|
+
0: 17, // "System instruction"
|
|
291
|
+
1: 12, // "AI message 1"
|
|
292
|
+
2: 15, // "Human message 1"
|
|
293
|
+
3: 12, // "AI message 2"
|
|
294
|
+
4: 15 // "Human message 2"
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Set a limit that can fit all messages
|
|
298
|
+
const result = getMessagesWithinTokenLimit({
|
|
299
|
+
messages,
|
|
300
|
+
maxContextTokens: 100,
|
|
301
|
+
indexTokenCountMap,
|
|
302
|
+
startOnMessageType: 'human'
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// All messages should be included since we're under the token limit
|
|
306
|
+
expect(result.context.length).toBe(5);
|
|
307
|
+
expect(result.context[0]).toBe(messages[0]); // System message
|
|
308
|
+
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
309
|
+
expect(result.context[2]).toBe(messages[2]); // Human message 1
|
|
310
|
+
expect(result.context[3]).toBe(messages[3]); // AI message 2
|
|
311
|
+
expect(result.context[4]).toBe(messages[4]); // Human message 2
|
|
312
|
+
|
|
313
|
+
// All messages should be included since we're under the token limit
|
|
314
|
+
expect(result.messagesToRefine.length).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should keep all messages if no message of required type is found', () => {
|
|
318
|
+
const messages = [
|
|
319
|
+
new SystemMessage('System instruction'),
|
|
320
|
+
new AIMessage('AI message 1'),
|
|
321
|
+
new AIMessage('AI message 2')
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const indexTokenCountMap = {
|
|
325
|
+
0: 17, // "System instruction"
|
|
326
|
+
1: 12, // "AI message 1"
|
|
327
|
+
2: 12 // "AI message 2"
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Set a limit that can fit all messages
|
|
331
|
+
const result = getMessagesWithinTokenLimit({
|
|
332
|
+
messages,
|
|
333
|
+
maxContextTokens: 100,
|
|
334
|
+
indexTokenCountMap,
|
|
335
|
+
startOnMessageType: 'human'
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Should include all messages since no human messages exist to start from
|
|
339
|
+
expect(result.context.length).toBe(3);
|
|
340
|
+
expect(result.context[0]).toBe(messages[0]); // System message
|
|
341
|
+
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
342
|
+
expect(result.context[2]).toBe(messages[2]); // AI message 2
|
|
343
|
+
|
|
344
|
+
expect(result.messagesToRefine.length).toBe(0);
|
|
345
|
+
});
|
|
265
346
|
});
|
|
266
347
|
|
|
267
348
|
describe('checkValidNumber', () => {
|
|
@@ -348,6 +429,46 @@ describe('Prune Messages Tests', () => {
|
|
|
348
429
|
expect(result.context[1]).toBe(messages[3]); // Message 2
|
|
349
430
|
expect(result.context[2]).toBe(messages[4]); // Response 2
|
|
350
431
|
});
|
|
432
|
+
|
|
433
|
+
it('should respect startOnMessageType parameter', () => {
|
|
434
|
+
const tokenCounter = createTestTokenCounter();
|
|
435
|
+
const messages = [
|
|
436
|
+
new SystemMessage('System instruction'),
|
|
437
|
+
new AIMessage('AI message 1'),
|
|
438
|
+
new HumanMessage('Human message 1'),
|
|
439
|
+
new AIMessage('AI message 2'),
|
|
440
|
+
new HumanMessage('Human message 2')
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
const indexTokenCountMap = {
|
|
444
|
+
0: tokenCounter(messages[0]),
|
|
445
|
+
1: tokenCounter(messages[1]),
|
|
446
|
+
2: tokenCounter(messages[2]),
|
|
447
|
+
3: tokenCounter(messages[3]),
|
|
448
|
+
4: tokenCounter(messages[4])
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// Set a limit that can fit all messages
|
|
452
|
+
const pruneMessages = createPruneMessages({
|
|
453
|
+
maxTokens: 100,
|
|
454
|
+
startIndex: 0,
|
|
455
|
+
tokenCounter,
|
|
456
|
+
indexTokenCountMap: { ...indexTokenCountMap }
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const result = pruneMessages({
|
|
460
|
+
messages,
|
|
461
|
+
startOnMessageType: 'human'
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// All messages should be included since we're under the token limit
|
|
465
|
+
expect(result.context.length).toBe(5);
|
|
466
|
+
expect(result.context[0]).toBe(messages[0]); // System message
|
|
467
|
+
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
468
|
+
expect(result.context[2]).toBe(messages[2]); // Human message 1
|
|
469
|
+
expect(result.context[3]).toBe(messages[3]); // AI message 2
|
|
470
|
+
expect(result.context[4]).toBe(messages[4]); // Human message 2
|
|
471
|
+
});
|
|
351
472
|
|
|
352
473
|
it('should update token counts when usage metadata is provided', () => {
|
|
353
474
|
const tokenCounter = createTestTokenCounter();
|
package/src/types/stream.ts
CHANGED
|
@@ -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
|
-
|
|
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> & {
|
package/src/types/tools.ts
CHANGED