@librechat/agents 3.0.42 → 3.0.43
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/agents/AgentContext.cjs +134 -70
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +5 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +85 -0
- package/dist/cjs/messages/tools.cjs.map +1 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +55 -32
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +30 -13
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +134 -70
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +82 -0
- package/dist/esm/messages/tools.mjs.map +1 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +54 -33
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +30 -13
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +37 -17
- package/dist/types/common/enum.d.ts +1 -1
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/tools.d.ts +17 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +15 -23
- package/dist/types/tools/ToolNode.d.ts +9 -7
- package/dist/types/types/tools.d.ts +5 -5
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +157 -85
- package/src/agents/__tests__/AgentContext.test.ts +805 -0
- package/src/common/enum.ts +1 -1
- package/src/graphs/Graph.ts +9 -21
- package/src/messages/__tests__/tools.test.ts +473 -0
- package/src/messages/index.ts +1 -0
- package/src/messages/tools.ts +99 -0
- package/src/scripts/code_exec_ptc.ts +78 -21
- package/src/scripts/programmatic_exec.ts +3 -3
- package/src/scripts/programmatic_exec_agent.ts +4 -4
- package/src/tools/ProgrammaticToolCalling.ts +71 -39
- package/src/tools/ToolNode.ts +33 -14
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +180 -5
- package/src/types/tools.ts +3 -5
package/src/common/enum.ts
CHANGED
|
@@ -160,7 +160,7 @@ export enum Constants {
|
|
|
160
160
|
OFFICIAL_CODE_BASEURL = 'https://api.librechat.ai/v1',
|
|
161
161
|
EXECUTE_CODE = 'execute_code',
|
|
162
162
|
TOOL_SEARCH_REGEX = 'tool_search_regex',
|
|
163
|
-
PROGRAMMATIC_TOOL_CALLING = '
|
|
163
|
+
PROGRAMMATIC_TOOL_CALLING = 'run_tools_with_code',
|
|
164
164
|
WEB_SEARCH = 'web_search',
|
|
165
165
|
CONTENT_AND_ARTIFACT = 'content_and_artifact',
|
|
166
166
|
LC_TRANSFER_TO_ = 'lc_transfer_to_',
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
GraphEvents,
|
|
36
36
|
Providers,
|
|
37
37
|
StepTypes,
|
|
38
|
-
Constants,
|
|
39
38
|
} from '@/common';
|
|
40
39
|
import {
|
|
41
40
|
formatAnthropicArtifactContent,
|
|
@@ -47,6 +46,7 @@ import {
|
|
|
47
46
|
formatContentStrings,
|
|
48
47
|
createPruneMessages,
|
|
49
48
|
addCacheControl,
|
|
49
|
+
extractToolDiscoveries,
|
|
50
50
|
} from '@/messages';
|
|
51
51
|
import {
|
|
52
52
|
resetIfNotEmpty,
|
|
@@ -457,8 +457,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
457
457
|
errorHandler: (data, metadata) =>
|
|
458
458
|
StandardGraph.handleToolCallErrorStatic(this, data, metadata),
|
|
459
459
|
toolRegistry: agentContext?.toolRegistry,
|
|
460
|
-
programmaticToolMap: agentContext?.getProgrammaticToolMap(),
|
|
461
|
-
programmaticToolDefs: agentContext?.getProgrammaticToolDefs(),
|
|
462
460
|
});
|
|
463
461
|
}
|
|
464
462
|
|
|
@@ -626,6 +624,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
626
624
|
throw new Error('No config provided');
|
|
627
625
|
}
|
|
628
626
|
|
|
627
|
+
const { messages } = state;
|
|
628
|
+
|
|
629
|
+
// Extract tool discoveries from current turn only (similar to formatArtifactPayload pattern)
|
|
630
|
+
const discoveredNames = extractToolDiscoveries(messages);
|
|
631
|
+
if (discoveredNames.length > 0) {
|
|
632
|
+
agentContext.markToolsAsDiscovered(discoveredNames);
|
|
633
|
+
}
|
|
634
|
+
|
|
629
635
|
const toolsForBinding = agentContext.getToolsForBinding();
|
|
630
636
|
let model =
|
|
631
637
|
this.overrideModel ??
|
|
@@ -646,7 +652,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
646
652
|
config.signal = this.signal;
|
|
647
653
|
}
|
|
648
654
|
this.config = config;
|
|
649
|
-
const { messages } = state;
|
|
650
655
|
|
|
651
656
|
let messagesToUse = messages;
|
|
652
657
|
if (
|
|
@@ -712,23 +717,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
712
717
|
|
|
713
718
|
const isLatestToolMessage = lastMessageY instanceof ToolMessage;
|
|
714
719
|
|
|
715
|
-
if (
|
|
716
|
-
isLatestToolMessage &&
|
|
717
|
-
lastMessageY.name === Constants.TOOL_SEARCH_REGEX &&
|
|
718
|
-
typeof lastMessageY.artifact === 'object' &&
|
|
719
|
-
lastMessageY.artifact != null
|
|
720
|
-
) {
|
|
721
|
-
const artifact = lastMessageY.artifact as {
|
|
722
|
-
tool_references?: Array<{ tool_name: string }>;
|
|
723
|
-
};
|
|
724
|
-
if (artifact.tool_references && artifact.tool_references.length > 0) {
|
|
725
|
-
const discoveredNames = artifact.tool_references.map(
|
|
726
|
-
(ref) => ref.tool_name
|
|
727
|
-
);
|
|
728
|
-
agentContext.markToolsAsDiscovered(discoveredNames);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
720
|
if (
|
|
733
721
|
isLatestToolMessage &&
|
|
734
722
|
agentContext.provider === Providers.ANTHROPIC
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// src/messages/__tests__/tools.test.ts
|
|
2
|
+
import {
|
|
3
|
+
AIMessageChunk,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
HumanMessage,
|
|
6
|
+
} from '@langchain/core/messages';
|
|
7
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
8
|
+
import { extractToolDiscoveries, hasToolSearchInCurrentTurn } from '../tools';
|
|
9
|
+
import { Constants } from '@/common';
|
|
10
|
+
|
|
11
|
+
describe('Tool Discovery Functions', () => {
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create an AIMessageChunk with tool calls
|
|
14
|
+
*/
|
|
15
|
+
const createAIMessage = (
|
|
16
|
+
content: string,
|
|
17
|
+
toolCalls: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
args: Record<string, unknown>;
|
|
21
|
+
}>
|
|
22
|
+
): AIMessageChunk => {
|
|
23
|
+
return new AIMessageChunk({
|
|
24
|
+
content,
|
|
25
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
26
|
+
id: tc.id,
|
|
27
|
+
name: tc.name,
|
|
28
|
+
args: tc.args,
|
|
29
|
+
type: 'tool_call' as const,
|
|
30
|
+
})),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Helper to create a ToolMessage (tool search result)
|
|
36
|
+
*/
|
|
37
|
+
const createToolSearchResult = (
|
|
38
|
+
toolCallId: string,
|
|
39
|
+
discoveredTools: string[]
|
|
40
|
+
): ToolMessage => {
|
|
41
|
+
return new ToolMessage({
|
|
42
|
+
content: `Found ${discoveredTools.length} tools`,
|
|
43
|
+
tool_call_id: toolCallId,
|
|
44
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
45
|
+
artifact: {
|
|
46
|
+
tool_references: discoveredTools.map((name) => ({
|
|
47
|
+
tool_name: name,
|
|
48
|
+
match_score: 0.9,
|
|
49
|
+
matched_field: 'description',
|
|
50
|
+
snippet: 'Test snippet',
|
|
51
|
+
})),
|
|
52
|
+
metadata: {
|
|
53
|
+
total_searched: 10,
|
|
54
|
+
pattern: 'test',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper to create a regular ToolMessage (non-search)
|
|
62
|
+
*/
|
|
63
|
+
const createRegularToolMessage = (
|
|
64
|
+
toolCallId: string,
|
|
65
|
+
name: string,
|
|
66
|
+
content: string
|
|
67
|
+
): ToolMessage => {
|
|
68
|
+
return new ToolMessage({
|
|
69
|
+
content,
|
|
70
|
+
tool_call_id: toolCallId,
|
|
71
|
+
name,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('extractToolDiscoveries', () => {
|
|
76
|
+
it('extracts tool names from a single tool search result', () => {
|
|
77
|
+
const messages: BaseMessage[] = [
|
|
78
|
+
new HumanMessage('Search for database tools'),
|
|
79
|
+
createAIMessage('Searching...', [
|
|
80
|
+
{
|
|
81
|
+
id: 'call_1',
|
|
82
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
83
|
+
args: { pattern: 'database' },
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
86
|
+
createToolSearchResult('call_1', ['database_query', 'database_insert']),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const result = extractToolDiscoveries(messages);
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual(['database_query', 'database_insert']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('extracts tool names from multiple tool search results in same turn', () => {
|
|
95
|
+
const messages: BaseMessage[] = [
|
|
96
|
+
new HumanMessage('Search for tools'),
|
|
97
|
+
createAIMessage('Searching...', [
|
|
98
|
+
{
|
|
99
|
+
id: 'call_1',
|
|
100
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
101
|
+
args: { pattern: 'database' },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'call_2',
|
|
105
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
106
|
+
args: { pattern: 'file' },
|
|
107
|
+
},
|
|
108
|
+
]),
|
|
109
|
+
createToolSearchResult('call_1', ['database_query']),
|
|
110
|
+
createToolSearchResult('call_2', ['file_read', 'file_write']),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = extractToolDiscoveries(messages);
|
|
114
|
+
|
|
115
|
+
expect(result).toEqual(['database_query', 'file_read', 'file_write']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns empty array when no messages', () => {
|
|
119
|
+
const result = extractToolDiscoveries([]);
|
|
120
|
+
expect(result).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns empty array when last message is not a ToolMessage', () => {
|
|
124
|
+
const messages: BaseMessage[] = [
|
|
125
|
+
new HumanMessage('Hello'),
|
|
126
|
+
createAIMessage('Hi there!', []),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const result = extractToolDiscoveries(messages);
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns empty array when no AI message with tool calls found', () => {
|
|
135
|
+
const messages: BaseMessage[] = [
|
|
136
|
+
new HumanMessage('Hello'),
|
|
137
|
+
new ToolMessage({
|
|
138
|
+
content: 'Some result',
|
|
139
|
+
tool_call_id: 'orphan_call',
|
|
140
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
141
|
+
}),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const result = extractToolDiscoveries(messages);
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('ignores tool search results from previous turns', () => {
|
|
150
|
+
const messages: BaseMessage[] = [
|
|
151
|
+
// Turn 1: Previous search
|
|
152
|
+
new HumanMessage('Search for old tools'),
|
|
153
|
+
createAIMessage('Searching...', [
|
|
154
|
+
{
|
|
155
|
+
id: 'old_call',
|
|
156
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
157
|
+
args: { pattern: 'old' },
|
|
158
|
+
},
|
|
159
|
+
]),
|
|
160
|
+
createToolSearchResult('old_call', ['old_tool_1', 'old_tool_2']),
|
|
161
|
+
// Turn 2: Current turn
|
|
162
|
+
new HumanMessage('Search for new tools'),
|
|
163
|
+
createAIMessage('Searching again...', [
|
|
164
|
+
{
|
|
165
|
+
id: 'new_call',
|
|
166
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
167
|
+
args: { pattern: 'new' },
|
|
168
|
+
},
|
|
169
|
+
]),
|
|
170
|
+
createToolSearchResult('new_call', ['new_tool']),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const result = extractToolDiscoveries(messages);
|
|
174
|
+
|
|
175
|
+
// Should only return tools from current turn
|
|
176
|
+
expect(result).toEqual(['new_tool']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('ignores non-search tool results', () => {
|
|
180
|
+
const messages: BaseMessage[] = [
|
|
181
|
+
new HumanMessage('Do some work'),
|
|
182
|
+
createAIMessage('Working...', [
|
|
183
|
+
{
|
|
184
|
+
id: 'search_call',
|
|
185
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
186
|
+
args: { pattern: 'test' },
|
|
187
|
+
},
|
|
188
|
+
{ id: 'other_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
189
|
+
]),
|
|
190
|
+
createToolSearchResult('search_call', ['found_tool']),
|
|
191
|
+
createRegularToolMessage('other_call', 'get_weather', '{"temp": 72}'),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const result = extractToolDiscoveries(messages);
|
|
195
|
+
|
|
196
|
+
expect(result).toEqual(['found_tool']);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles empty tool_references in artifact', () => {
|
|
200
|
+
const messages: BaseMessage[] = [
|
|
201
|
+
new HumanMessage('Search'),
|
|
202
|
+
createAIMessage('Searching...', [
|
|
203
|
+
{
|
|
204
|
+
id: 'call_1',
|
|
205
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
206
|
+
args: { pattern: 'xyz' },
|
|
207
|
+
},
|
|
208
|
+
]),
|
|
209
|
+
new ToolMessage({
|
|
210
|
+
content: 'No tools found',
|
|
211
|
+
tool_call_id: 'call_1',
|
|
212
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
213
|
+
artifact: {
|
|
214
|
+
tool_references: [],
|
|
215
|
+
metadata: { total_searched: 10, pattern: 'xyz' },
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const result = extractToolDiscoveries(messages);
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles missing artifact', () => {
|
|
226
|
+
const messages: BaseMessage[] = [
|
|
227
|
+
new HumanMessage('Search'),
|
|
228
|
+
createAIMessage('Searching...', [
|
|
229
|
+
{
|
|
230
|
+
id: 'call_1',
|
|
231
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
232
|
+
args: { pattern: 'test' },
|
|
233
|
+
},
|
|
234
|
+
]),
|
|
235
|
+
new ToolMessage({
|
|
236
|
+
content: 'Error occurred',
|
|
237
|
+
tool_call_id: 'call_1',
|
|
238
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
239
|
+
// No artifact
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const result = extractToolDiscoveries(messages);
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('ignores tool messages with wrong tool_call_id', () => {
|
|
249
|
+
const messages: BaseMessage[] = [
|
|
250
|
+
new HumanMessage('Search'),
|
|
251
|
+
createAIMessage('Searching...', [
|
|
252
|
+
{
|
|
253
|
+
id: 'call_1',
|
|
254
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
255
|
+
args: { pattern: 'test' },
|
|
256
|
+
},
|
|
257
|
+
]),
|
|
258
|
+
// This has a different tool_call_id that doesn't match the AI message
|
|
259
|
+
createToolSearchResult('wrong_id', ['should_not_appear']),
|
|
260
|
+
createToolSearchResult('call_1', ['correct_tool']),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const result = extractToolDiscoveries(messages);
|
|
264
|
+
|
|
265
|
+
expect(result).toEqual(['correct_tool']);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('only looks at messages after the latest AI parent', () => {
|
|
269
|
+
const messages: BaseMessage[] = [
|
|
270
|
+
// First AI message with search
|
|
271
|
+
createAIMessage('First search', [
|
|
272
|
+
{
|
|
273
|
+
id: 'first_call',
|
|
274
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
275
|
+
args: { pattern: 'first' },
|
|
276
|
+
},
|
|
277
|
+
]),
|
|
278
|
+
createToolSearchResult('first_call', ['first_tool']),
|
|
279
|
+
// Second AI message - this is the "latest AI parent" for the last tool message
|
|
280
|
+
createAIMessage('Second search', [
|
|
281
|
+
{
|
|
282
|
+
id: 'second_call',
|
|
283
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
284
|
+
args: { pattern: 'second' },
|
|
285
|
+
},
|
|
286
|
+
]),
|
|
287
|
+
createToolSearchResult('second_call', ['second_tool']),
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const result = extractToolDiscoveries(messages);
|
|
291
|
+
|
|
292
|
+
// Should only find second_tool (from the turn of the latest AI parent)
|
|
293
|
+
expect(result).toEqual(['second_tool']);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('hasToolSearchInCurrentTurn', () => {
|
|
298
|
+
it('returns true when current turn has tool search results', () => {
|
|
299
|
+
const messages: BaseMessage[] = [
|
|
300
|
+
new HumanMessage('Search'),
|
|
301
|
+
createAIMessage('Searching...', [
|
|
302
|
+
{
|
|
303
|
+
id: 'call_1',
|
|
304
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
305
|
+
args: { pattern: 'test' },
|
|
306
|
+
},
|
|
307
|
+
]),
|
|
308
|
+
createToolSearchResult('call_1', ['found_tool']),
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
312
|
+
|
|
313
|
+
expect(result).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('returns false when no messages', () => {
|
|
317
|
+
const result = hasToolSearchInCurrentTurn([]);
|
|
318
|
+
expect(result).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('returns false when last message is not a ToolMessage', () => {
|
|
322
|
+
const messages: BaseMessage[] = [
|
|
323
|
+
new HumanMessage('Hello'),
|
|
324
|
+
createAIMessage('Hi!', []),
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
328
|
+
|
|
329
|
+
expect(result).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('returns false when no AI parent found', () => {
|
|
333
|
+
const messages: BaseMessage[] = [
|
|
334
|
+
new HumanMessage('Hello'),
|
|
335
|
+
new ToolMessage({
|
|
336
|
+
content: 'Result',
|
|
337
|
+
tool_call_id: 'orphan',
|
|
338
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
339
|
+
}),
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
343
|
+
|
|
344
|
+
expect(result).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('returns false when current turn has no tool search (only other tools)', () => {
|
|
348
|
+
const messages: BaseMessage[] = [
|
|
349
|
+
new HumanMessage('Get weather'),
|
|
350
|
+
createAIMessage('Getting weather...', [
|
|
351
|
+
{ id: 'call_1', name: 'get_weather', args: { city: 'NYC' } },
|
|
352
|
+
]),
|
|
353
|
+
createRegularToolMessage('call_1', 'get_weather', '{"temp": 72}'),
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
357
|
+
|
|
358
|
+
expect(result).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('returns true even with mixed tool types in current turn', () => {
|
|
362
|
+
const messages: BaseMessage[] = [
|
|
363
|
+
new HumanMessage('Search and get weather'),
|
|
364
|
+
createAIMessage('Working...', [
|
|
365
|
+
{
|
|
366
|
+
id: 'search_call',
|
|
367
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
368
|
+
args: { pattern: 'test' },
|
|
369
|
+
},
|
|
370
|
+
{ id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
371
|
+
]),
|
|
372
|
+
createRegularToolMessage('weather_call', 'get_weather', '{"temp": 72}'),
|
|
373
|
+
createToolSearchResult('search_call', ['found_tool']),
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
377
|
+
|
|
378
|
+
expect(result).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('returns false for tool search from previous turn', () => {
|
|
382
|
+
const messages: BaseMessage[] = [
|
|
383
|
+
// Previous turn with search
|
|
384
|
+
createAIMessage('Searching...', [
|
|
385
|
+
{
|
|
386
|
+
id: 'old_call',
|
|
387
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
388
|
+
args: { pattern: 'old' },
|
|
389
|
+
},
|
|
390
|
+
]),
|
|
391
|
+
createToolSearchResult('old_call', ['old_tool']),
|
|
392
|
+
// Current turn without search
|
|
393
|
+
new HumanMessage('Get weather now'),
|
|
394
|
+
createAIMessage('Getting weather...', [
|
|
395
|
+
{ id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
396
|
+
]),
|
|
397
|
+
createRegularToolMessage('weather_call', 'get_weather', '{"temp": 72}'),
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
401
|
+
|
|
402
|
+
expect(result).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('Integration: extractToolDiscoveries + hasToolSearchInCurrentTurn', () => {
|
|
407
|
+
it('hasToolSearchInCurrentTurn is true when extractToolDiscoveries returns results', () => {
|
|
408
|
+
const messages: BaseMessage[] = [
|
|
409
|
+
new HumanMessage('Search'),
|
|
410
|
+
createAIMessage('Searching...', [
|
|
411
|
+
{
|
|
412
|
+
id: 'call_1',
|
|
413
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
414
|
+
args: { pattern: 'test' },
|
|
415
|
+
},
|
|
416
|
+
]),
|
|
417
|
+
createToolSearchResult('call_1', ['tool_a', 'tool_b']),
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
const hasSearch = hasToolSearchInCurrentTurn(messages);
|
|
421
|
+
const discoveries = extractToolDiscoveries(messages);
|
|
422
|
+
|
|
423
|
+
expect(hasSearch).toBe(true);
|
|
424
|
+
expect(discoveries.length).toBeGreaterThan(0);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('both return empty/false for non-search turns', () => {
|
|
428
|
+
const messages: BaseMessage[] = [
|
|
429
|
+
new HumanMessage('Get weather'),
|
|
430
|
+
createAIMessage('Getting...', [
|
|
431
|
+
{ id: 'call_1', name: 'get_weather', args: { city: 'NYC' } },
|
|
432
|
+
]),
|
|
433
|
+
createRegularToolMessage('call_1', 'get_weather', '{"temp": 72}'),
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
const hasSearch = hasToolSearchInCurrentTurn(messages);
|
|
437
|
+
const discoveries = extractToolDiscoveries(messages);
|
|
438
|
+
|
|
439
|
+
expect(hasSearch).toBe(false);
|
|
440
|
+
expect(discoveries).toEqual([]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('hasToolSearchInCurrentTurn can be used as quick check before extraction', () => {
|
|
444
|
+
const messagesWithSearch: BaseMessage[] = [
|
|
445
|
+
new HumanMessage('Search'),
|
|
446
|
+
createAIMessage('Searching...', [
|
|
447
|
+
{
|
|
448
|
+
id: 'call_1',
|
|
449
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
450
|
+
args: { pattern: 'test' },
|
|
451
|
+
},
|
|
452
|
+
]),
|
|
453
|
+
createToolSearchResult('call_1', ['tool_a']),
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
const messagesWithoutSearch: BaseMessage[] = [
|
|
457
|
+
new HumanMessage('Hello'),
|
|
458
|
+
createAIMessage('Hi!', []),
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
// Pattern: quick check first, then extract only if needed
|
|
462
|
+
if (hasToolSearchInCurrentTurn(messagesWithSearch)) {
|
|
463
|
+
const discoveries = extractToolDiscoveries(messagesWithSearch);
|
|
464
|
+
expect(discoveries).toEqual(['tool_a']);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (hasToolSearchInCurrentTurn(messagesWithoutSearch)) {
|
|
468
|
+
// This should not execute
|
|
469
|
+
expect(true).toBe(false);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
package/src/messages/index.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/messages/toolDiscovery.ts
|
|
2
|
+
import { AIMessageChunk, ToolMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
import { Constants } from '@/common';
|
|
5
|
+
import { findLastIndex } from './core';
|
|
6
|
+
|
|
7
|
+
type ToolSearchArtifact = {
|
|
8
|
+
tool_references?: Array<{ tool_name: string }>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts discovered tool names from tool search results in the current turn.
|
|
13
|
+
* Only processes tool search messages after the latest AI message with tool calls.
|
|
14
|
+
*
|
|
15
|
+
* Similar pattern to formatArtifactPayload - finds relevant messages efficiently
|
|
16
|
+
* by identifying the latest AI parent and only processing subsequent tool messages.
|
|
17
|
+
*
|
|
18
|
+
* @param messages - All messages in the conversation
|
|
19
|
+
* @returns Array of discovered tool names (empty if no new discoveries)
|
|
20
|
+
*/
|
|
21
|
+
export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
22
|
+
const lastMessage = messages[messages.length - 1];
|
|
23
|
+
if (!(lastMessage instanceof ToolMessage)) return [];
|
|
24
|
+
|
|
25
|
+
// Find the latest AIMessage with tool_calls that this tool message belongs to
|
|
26
|
+
const latestAIParentIndex = findLastIndex(
|
|
27
|
+
messages,
|
|
28
|
+
(msg) =>
|
|
29
|
+
(msg instanceof AIMessageChunk &&
|
|
30
|
+
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
31
|
+
msg.tool_calls?.some((tc) => tc.id === lastMessage.tool_call_id)) ??
|
|
32
|
+
false
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (latestAIParentIndex === -1) return [];
|
|
36
|
+
|
|
37
|
+
// Collect tool_call_ids from the AI message
|
|
38
|
+
const aiMessage = messages[latestAIParentIndex] as AIMessageChunk;
|
|
39
|
+
const toolCallIds = new Set(aiMessage.tool_calls?.map((tc) => tc.id) ?? []);
|
|
40
|
+
|
|
41
|
+
// Only process tool search results after the AI message that belong to this turn
|
|
42
|
+
const discoveredNames: string[] = [];
|
|
43
|
+
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
44
|
+
const msg = messages[i];
|
|
45
|
+
if (!(msg instanceof ToolMessage)) continue;
|
|
46
|
+
if (msg.name !== Constants.TOOL_SEARCH_REGEX) continue;
|
|
47
|
+
if (!toolCallIds.has(msg.tool_call_id)) continue;
|
|
48
|
+
|
|
49
|
+
// This is a tool search result from the current turn
|
|
50
|
+
if (typeof msg.artifact === 'object' && msg.artifact != null) {
|
|
51
|
+
const artifact = msg.artifact as ToolSearchArtifact;
|
|
52
|
+
if (artifact.tool_references && artifact.tool_references.length > 0) {
|
|
53
|
+
for (const ref of artifact.tool_references) {
|
|
54
|
+
discoveredNames.push(ref.tool_name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return discoveredNames;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks if the current turn has any tool search results.
|
|
65
|
+
* Quick check to avoid full extraction when not needed.
|
|
66
|
+
*/
|
|
67
|
+
export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
|
|
68
|
+
const lastMessage = messages[messages.length - 1];
|
|
69
|
+
if (!(lastMessage instanceof ToolMessage)) return false;
|
|
70
|
+
|
|
71
|
+
// Find the latest AIMessage with tool_calls
|
|
72
|
+
const latestAIParentIndex = findLastIndex(
|
|
73
|
+
messages,
|
|
74
|
+
(msg) =>
|
|
75
|
+
(msg instanceof AIMessageChunk &&
|
|
76
|
+
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
77
|
+
msg.tool_calls?.some((tc) => tc.id === lastMessage.tool_call_id)) ??
|
|
78
|
+
false
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (latestAIParentIndex === -1) return false;
|
|
82
|
+
|
|
83
|
+
const aiMessage = messages[latestAIParentIndex] as AIMessageChunk;
|
|
84
|
+
const toolCallIds = new Set(aiMessage.tool_calls?.map((tc) => tc.id) ?? []);
|
|
85
|
+
|
|
86
|
+
// Check if any tool search results exist after the AI message
|
|
87
|
+
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
88
|
+
const msg = messages[i];
|
|
89
|
+
if (
|
|
90
|
+
msg instanceof ToolMessage &&
|
|
91
|
+
msg.name === Constants.TOOL_SEARCH_REGEX &&
|
|
92
|
+
toolCallIds.has(msg.tool_call_id)
|
|
93
|
+
) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|