@librechat/agents 2.2.5 → 2.2.7
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/events.cjs +3 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +40 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/fake.cjs +45 -3
- package/dist/cjs/llm/fake.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +161 -80
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +9 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/events.mjs +3 -0
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +40 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/fake.mjs +45 -3
- package/dist/esm/llm/fake.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +162 -81
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +9 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +3 -1
- package/dist/types/llm/fake.d.ts +14 -4
- package/dist/types/messages/format.d.ts +6 -15
- package/dist/types/tools/ToolNode.d.ts +2 -1
- package/dist/types/types/stream.d.ts +35 -1
- package/dist/types/types/tools.d.ts +3 -5
- package/package.json +6 -6
- package/src/events.ts +4 -0
- package/src/graphs/Graph.ts +52 -2
- package/src/llm/fake.ts +58 -8
- 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/scripts/image.ts +2 -2
- package/src/specs/tool-error.test.ts +156 -0
- package/src/tools/ToolNode.ts +9 -0
- package/src/types/stream.ts +41 -2
- package/src/types/tools.ts +3 -6
|
@@ -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/scripts/image.ts
CHANGED
|
@@ -65,8 +65,8 @@ async function testCodeExecution(): Promise<void> {
|
|
|
65
65
|
graphConfig: {
|
|
66
66
|
type: 'standard',
|
|
67
67
|
llmConfig,
|
|
68
|
-
|
|
69
|
-
tools: [fetchRandomImageURL],
|
|
68
|
+
tools: [fetchRandomImageTool],
|
|
69
|
+
// tools: [fetchRandomImageURL],
|
|
70
70
|
instructions: 'You are a friendly AI assistant with internet capabilities. Always address the user by their name.',
|
|
71
71
|
additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
|
|
72
72
|
},
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { config } from 'dotenv';
|
|
5
|
+
config();
|
|
6
|
+
import { tool } from '@langchain/core/tools';
|
|
7
|
+
import { ToolCall } from '@langchain/core/messages/tool';
|
|
8
|
+
import { HumanMessage, BaseMessage } from '@langchain/core/messages';
|
|
9
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
10
|
+
import type * as t from '@/types';
|
|
11
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
12
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
13
|
+
import { GraphEvents, Providers } from '@/common';
|
|
14
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
15
|
+
import { getArgs } from '@/scripts/args';
|
|
16
|
+
import { StandardGraph } from '@/graphs';
|
|
17
|
+
import { Run } from '@/run';
|
|
18
|
+
|
|
19
|
+
const errorTool = tool(
|
|
20
|
+
async () => {
|
|
21
|
+
throw new Error('this is a test error I threw on purpose');
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'errorTool',
|
|
25
|
+
description: 'A tool that always throws an error',
|
|
26
|
+
schema: z.object({ input: z.string().optional() }),
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
describe('Tool Error Handling Tests', () => {
|
|
31
|
+
jest.setTimeout(30000);
|
|
32
|
+
let run: Run<t.IState>;
|
|
33
|
+
let contentParts: t.MessageContentComplex[];
|
|
34
|
+
let conversationHistory: BaseMessage[];
|
|
35
|
+
let aggregateContent: t.ContentAggregator;
|
|
36
|
+
let handleToolCallErrorSpy: jest.SpyInstance;
|
|
37
|
+
|
|
38
|
+
const config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string; streamMode: string } = {
|
|
39
|
+
configurable: {
|
|
40
|
+
thread_id: 'conversation-num-1',
|
|
41
|
+
},
|
|
42
|
+
streamMode: 'values',
|
|
43
|
+
version: 'v2' as const,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
conversationHistory = [];
|
|
48
|
+
const { contentParts: parts, aggregateContent: ac } = createContentAggregator();
|
|
49
|
+
aggregateContent = ac;
|
|
50
|
+
contentParts = parts as t.MessageContentComplex[];
|
|
51
|
+
handleToolCallErrorSpy = jest.spyOn(StandardGraph.prototype, 'handleToolCallError');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
handleToolCallErrorSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const onMessageDeltaSpy = jest.fn();
|
|
59
|
+
const onRunStepSpy = jest.fn();
|
|
60
|
+
const onRunStepCompletedSpy = jest.fn();
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
onMessageDeltaSpy.mockReset();
|
|
64
|
+
onRunStepSpy.mockReset();
|
|
65
|
+
onRunStepCompletedSpy.mockReset();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const setupCustomHandlers = (): Record<string | GraphEvents, t.EventHandler> => ({
|
|
69
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
70
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
71
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
72
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
73
|
+
handle: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData): void => {
|
|
74
|
+
if ((data.result as t.MessageContentComplex)?.['type'] === 'tool_call') {
|
|
75
|
+
run.Graph?.overrideTestModel(['Looks like there was an error calling the tool.'], 5);
|
|
76
|
+
}
|
|
77
|
+
onRunStepCompletedSpy(event, data);
|
|
78
|
+
aggregateContent({ event, data: data as unknown as { result: t.ToolEndEvent; } });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
82
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData, metadata, graph): void => {
|
|
83
|
+
const runStepData = data as t.RunStep;
|
|
84
|
+
onRunStepSpy(event, runStepData, metadata, graph);
|
|
85
|
+
aggregateContent({ event, data: runStepData });
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
89
|
+
handle: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData): void => {
|
|
90
|
+
aggregateContent({ event, data: data as t.RunStepDeltaEvent });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
94
|
+
handle: (event: GraphEvents.ON_MESSAGE_DELTA, data: t.StreamEventData, metadata, graph): void => {
|
|
95
|
+
onMessageDeltaSpy(event, data, metadata, graph);
|
|
96
|
+
aggregateContent({ event, data: data as t.MessageDeltaEvent });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should handle tool call errors correctly', async () => {
|
|
102
|
+
const { userName, location } = await getArgs();
|
|
103
|
+
const llmConfig = getLLMConfig(Providers.OPENAI);
|
|
104
|
+
const customHandlers = setupCustomHandlers();
|
|
105
|
+
|
|
106
|
+
// Create the run instance
|
|
107
|
+
run = await Run.create<t.IState>({
|
|
108
|
+
runId: 'test-run-id',
|
|
109
|
+
graphConfig: {
|
|
110
|
+
type: 'standard',
|
|
111
|
+
llmConfig,
|
|
112
|
+
tools: [errorTool],
|
|
113
|
+
instructions: 'You are a helpful AI assistant.',
|
|
114
|
+
additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
|
|
115
|
+
},
|
|
116
|
+
returnContent: true,
|
|
117
|
+
customHandlers,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const toolCalls: ToolCall[] = [
|
|
121
|
+
{
|
|
122
|
+
name: "errorTool",
|
|
123
|
+
args: {
|
|
124
|
+
input: "test input",
|
|
125
|
+
},
|
|
126
|
+
id: "call_test123",
|
|
127
|
+
type: "tool_call",
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const firstResponse = 'Let me try calling the tool';
|
|
132
|
+
run.Graph?.overrideTestModel([firstResponse], 5, toolCalls);
|
|
133
|
+
|
|
134
|
+
const userMessage = 'Use the error tool';
|
|
135
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
136
|
+
|
|
137
|
+
const inputs = {
|
|
138
|
+
messages: conversationHistory,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await run.processStream(inputs, config);
|
|
142
|
+
|
|
143
|
+
// Verify handleToolCallError was called
|
|
144
|
+
expect(handleToolCallErrorSpy).toHaveBeenCalled();
|
|
145
|
+
|
|
146
|
+
// Find the tool call content part
|
|
147
|
+
const toolCallPart = contentParts.find(part =>
|
|
148
|
+
part?.type === 'tool_call'
|
|
149
|
+
) as t.ToolCallContent | undefined;
|
|
150
|
+
|
|
151
|
+
// Verify the error message in contentParts
|
|
152
|
+
expect(toolCallPart).toBeDefined();
|
|
153
|
+
expect(toolCallPart?.tool_call?.output).toContain('Error processing tool');
|
|
154
|
+
expect(toolCallPart?.tool_call?.output).toContain('this is a test error I threw on purpose');
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -14,12 +14,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
14
14
|
private loadRuntimeTools?: t.ToolRefGenerator;
|
|
15
15
|
handleToolErrors = true;
|
|
16
16
|
toolCallStepIds?: Map<string, string>;
|
|
17
|
+
errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
|
|
17
18
|
|
|
18
19
|
constructor({
|
|
19
20
|
tools,
|
|
20
21
|
toolMap,
|
|
21
22
|
name,
|
|
22
23
|
tags,
|
|
24
|
+
errorHandler,
|
|
23
25
|
toolCallStepIds,
|
|
24
26
|
handleToolErrors,
|
|
25
27
|
loadRuntimeTools,
|
|
@@ -30,6 +32,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
30
32
|
this.toolCallStepIds = toolCallStepIds;
|
|
31
33
|
this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
|
|
32
34
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
35
|
+
this.errorHandler = errorHandler;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -83,6 +86,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
83
86
|
if (isGraphInterrupt(e)) {
|
|
84
87
|
throw e;
|
|
85
88
|
}
|
|
89
|
+
this.errorHandler?.({
|
|
90
|
+
error: e,
|
|
91
|
+
id: call.id!,
|
|
92
|
+
name: call.name,
|
|
93
|
+
input: call.args,
|
|
94
|
+
}, config?.metadata);
|
|
86
95
|
return new ToolMessage({
|
|
87
96
|
content: `Error: ${e.message}\n Please fix your mistakes.`,
|
|
88
97
|
name: call.name,
|
package/src/types/stream.ts
CHANGED
|
@@ -93,6 +93,11 @@ export type MessageCreationDetails = {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
export type ToolEndData = { input: string | Record<string, unknown>, output?: ToolMessage };
|
|
96
|
+
export type ToolErrorData = {
|
|
97
|
+
id: string,
|
|
98
|
+
name: string,
|
|
99
|
+
error?: Error,
|
|
100
|
+
} & Pick<ToolEndData, 'input'>;
|
|
96
101
|
export type ToolEndCallback = (data: ToolEndData, metadata?: Record<string, unknown>) => void;
|
|
97
102
|
|
|
98
103
|
export type ProcessedToolCall = {
|
|
@@ -240,8 +245,35 @@ export type BedrockReasoningContentText = {
|
|
|
240
245
|
reasoningText: { text?: string; signature?: string; }
|
|
241
246
|
};
|
|
242
247
|
|
|
248
|
+
/**
|
|
249
|
+
* A call to a tool.
|
|
250
|
+
*/
|
|
251
|
+
export type ToolCallPart = {
|
|
252
|
+
/** Type ("tool_call") according to Assistants Tool Call Structure */
|
|
253
|
+
type: ContentTypes.TOOL_CALL;
|
|
254
|
+
/** The name of the tool to be called */
|
|
255
|
+
name: string;
|
|
256
|
+
/** The arguments to the tool call */
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
|
+
args?: string | Record<string, any>;
|
|
259
|
+
|
|
260
|
+
/** If provided, an identifier associated with the tool call */
|
|
261
|
+
id?: string;
|
|
262
|
+
/** If provided, the output of the tool call */
|
|
263
|
+
output?: string;
|
|
264
|
+
/** Auth URL */
|
|
265
|
+
auth?: string;
|
|
266
|
+
/** Expiration time */
|
|
267
|
+
expires_at?: number;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export type ToolCallContent = {
|
|
271
|
+
type: ContentTypes.TOOL_CALL;
|
|
272
|
+
tool_call?: ToolCallPart;
|
|
273
|
+
};
|
|
274
|
+
|
|
243
275
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
244
|
-
export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
|
|
276
|
+
export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ToolCallContent | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
|
|
245
277
|
type?: 'text' | 'image_url' | 'think' | 'thinking' | string;
|
|
246
278
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
279
|
}) | (Record<string, any> & {
|
|
@@ -249,7 +281,14 @@ export type MessageContentComplex = (ThinkingContentText | AgentUpdate | Reasoni
|
|
|
249
281
|
})) & {
|
|
250
282
|
tool_call_ids?: string[];
|
|
251
283
|
};
|
|
252
|
-
|
|
284
|
+
|
|
285
|
+
export interface TMessage {
|
|
286
|
+
role?: string;
|
|
287
|
+
content?: MessageContentComplex[] | string;
|
|
288
|
+
[key: string]: any;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type TPayload = Array<Partial<TMessage>>;
|
|
253
292
|
|
|
254
293
|
export type CustomChunk = Partial<OpenAITypes.ChatCompletionChunk> & {
|
|
255
294
|
choices?: Partial<Array<Partial<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice> & {
|
package/src/types/tools.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import type { RunnableToolLike } from '@langchain/core/runnables';
|
|
3
3
|
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
4
4
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
|
-
import {
|
|
5
|
+
import type { ToolErrorData } from './stream';
|
|
6
|
+
import { EnvVar } from '@/common';
|
|
6
7
|
|
|
7
8
|
/** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
|
|
8
9
|
export type CustomToolCall = {
|
|
@@ -29,6 +30,7 @@ export type ToolNodeOptions = {
|
|
|
29
30
|
handleToolErrors?: boolean;
|
|
30
31
|
loadRuntimeTools?: ToolRefGenerator;
|
|
31
32
|
toolCallStepIds?: Map<string, string>;
|
|
33
|
+
errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => void
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
|
|
@@ -42,11 +44,6 @@ export type ToolEndEvent = {
|
|
|
42
44
|
index: number;
|
|
43
45
|
};
|
|
44
46
|
|
|
45
|
-
export type ToolCallContent = {
|
|
46
|
-
type: ContentTypes.TOOL_CALL;
|
|
47
|
-
tool_call: ToolCall;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
47
|
export type CodeEnvFile = {
|
|
51
48
|
id: string;
|
|
52
49
|
name: string;
|