@librechat/agents 3.1.1 → 3.1.21
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 +9 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +17 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +66 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/createSchemaOnlyTool.cjs +31 -0
- package/dist/cjs/tools/createSchemaOnlyTool.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +9 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +17 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +67 -6
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/createSchemaOnlyTool.mjs +28 -0
- package/dist/esm/tools/createSchemaOnlyTool.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +7 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/tools/ToolNode.d.ts +10 -1
- package/dist/types/tools/createSchemaOnlyTool.d.ts +12 -0
- package/dist/types/types/graph.d.ts +6 -0
- package/dist/types/types/tools.d.ts +49 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +10 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +22 -0
- package/src/index.ts +2 -0
- package/src/specs/azure.simple.test.ts +214 -175
- package/src/tools/ToolNode.ts +95 -15
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +10 -9
- package/src/tools/__tests__/ToolSearch.integration.test.ts +10 -9
- package/src/tools/createSchemaOnlyTool.ts +37 -0
- package/src/types/graph.ts +6 -0
- package/src/types/tools.ts +52 -0
|
@@ -19,7 +19,6 @@ import { ContentTypes, GraphEvents, Providers, TitleMethod } from '@/common';
|
|
|
19
19
|
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
20
20
|
import { capitalizeFirstLetter } from './spec.utils';
|
|
21
21
|
import { getLLMConfig } from '@/utils/llmConfig';
|
|
22
|
-
import { getArgs } from '@/scripts/args';
|
|
23
22
|
import { Run } from '@/run';
|
|
24
23
|
|
|
25
24
|
// Auto-skip this suite if Azure env vars are not present
|
|
@@ -34,15 +33,24 @@ const hasAzure = requiredAzureEnv.every(
|
|
|
34
33
|
);
|
|
35
34
|
const describeIfAzure = hasAzure ? describe : describe.skip;
|
|
36
35
|
|
|
36
|
+
const isContentFilterError = (error: unknown): boolean => {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return (
|
|
39
|
+
message.includes('content management policy') ||
|
|
40
|
+
message.includes('content filtering')
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
37
44
|
const provider = Providers.AZURE;
|
|
45
|
+
let contentFilterTriggered = false;
|
|
38
46
|
describeIfAzure(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
39
47
|
jest.setTimeout(30000);
|
|
40
48
|
let run: Run<t.IState>;
|
|
41
|
-
let runningHistory: BaseMessage[];
|
|
42
49
|
let collectedUsage: UsageMetadata[];
|
|
43
50
|
let conversationHistory: BaseMessage[];
|
|
44
51
|
let aggregateContent: t.ContentAggregator;
|
|
45
52
|
let contentParts: t.MessageContentComplex[];
|
|
53
|
+
let runningHistory: BaseMessage[] | null = null;
|
|
46
54
|
|
|
47
55
|
const config = {
|
|
48
56
|
configurable: {
|
|
@@ -129,186 +137,217 @@ describeIfAzure(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
129
137
|
});
|
|
130
138
|
|
|
131
139
|
test(`${capitalizeFirstLetter(provider)}: should process a simple message, generate title`, async () => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const userMessage = 'hi';
|
|
151
|
-
conversationHistory.push(new HumanMessage(userMessage));
|
|
152
|
-
|
|
153
|
-
const inputs = {
|
|
154
|
-
messages: conversationHistory,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const finalContentParts = await run.processStream(inputs, config);
|
|
158
|
-
expect(finalContentParts).toBeDefined();
|
|
159
|
-
const allTextParts = finalContentParts?.every(
|
|
160
|
-
(part) => part.type === ContentTypes.TEXT
|
|
161
|
-
);
|
|
162
|
-
expect(allTextParts).toBe(true);
|
|
163
|
-
expect(collectedUsage.length).toBeGreaterThan(0);
|
|
164
|
-
expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
|
|
165
|
-
expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
|
|
166
|
-
|
|
167
|
-
const finalMessages = run.getRunMessages();
|
|
168
|
-
expect(finalMessages).toBeDefined();
|
|
169
|
-
conversationHistory.push(...(finalMessages ?? []));
|
|
170
|
-
expect(conversationHistory.length).toBeGreaterThan(1);
|
|
171
|
-
runningHistory = conversationHistory.slice();
|
|
172
|
-
|
|
173
|
-
expect(onMessageDeltaSpy).toHaveBeenCalled();
|
|
174
|
-
expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
|
|
175
|
-
expect(onMessageDeltaSpy.mock.calls[0][3]).toBeDefined(); // Graph exists
|
|
176
|
-
|
|
177
|
-
expect(onRunStepSpy).toHaveBeenCalled();
|
|
178
|
-
expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
|
|
179
|
-
expect(onRunStepSpy.mock.calls[0][3]).toBeDefined(); // Graph exists
|
|
180
|
-
|
|
181
|
-
const { handleLLMEnd, collected } = createMetadataAggregator();
|
|
182
|
-
const titleResult = await run.generateTitle({
|
|
183
|
-
provider,
|
|
184
|
-
inputText: userMessage,
|
|
185
|
-
titleMethod: TitleMethod.STRUCTURED,
|
|
186
|
-
contentParts,
|
|
187
|
-
clientOptions: llmConfig,
|
|
188
|
-
chainOptions: {
|
|
189
|
-
callbacks: [
|
|
190
|
-
{
|
|
191
|
-
handleLLMEnd,
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
});
|
|
140
|
+
try {
|
|
141
|
+
const llmConfig = getLLMConfig(provider);
|
|
142
|
+
const customHandlers = setupCustomHandlers();
|
|
143
|
+
|
|
144
|
+
run = await Run.create<t.IState>({
|
|
145
|
+
runId: 'test-run-id',
|
|
146
|
+
graphConfig: {
|
|
147
|
+
type: 'standard',
|
|
148
|
+
llmConfig,
|
|
149
|
+
tools: [new Calculator()],
|
|
150
|
+
instructions:
|
|
151
|
+
'You are a helpful AI assistant. Keep responses concise and friendly.',
|
|
152
|
+
},
|
|
153
|
+
returnContent: true,
|
|
154
|
+
customHandlers,
|
|
155
|
+
});
|
|
196
156
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
157
|
+
const userMessage = 'Hello, how are you today?';
|
|
158
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
159
|
+
|
|
160
|
+
const inputs = {
|
|
161
|
+
messages: conversationHistory,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const finalContentParts = await run.processStream(inputs, config);
|
|
165
|
+
expect(finalContentParts).toBeDefined();
|
|
166
|
+
const allTextParts = finalContentParts?.every(
|
|
167
|
+
(part) => part.type === ContentTypes.TEXT
|
|
168
|
+
);
|
|
169
|
+
expect(allTextParts).toBe(true);
|
|
170
|
+
expect(collectedUsage.length).toBeGreaterThan(0);
|
|
171
|
+
expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
|
|
172
|
+
expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
|
|
173
|
+
|
|
174
|
+
const finalMessages = run.getRunMessages();
|
|
175
|
+
expect(finalMessages).toBeDefined();
|
|
176
|
+
conversationHistory.push(...(finalMessages ?? []));
|
|
177
|
+
expect(conversationHistory.length).toBeGreaterThan(1);
|
|
178
|
+
runningHistory = conversationHistory.slice();
|
|
179
|
+
|
|
180
|
+
expect(onMessageDeltaSpy).toHaveBeenCalled();
|
|
181
|
+
expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
|
|
182
|
+
expect(onMessageDeltaSpy.mock.calls[0][3]).toBeDefined(); // Graph exists
|
|
183
|
+
|
|
184
|
+
expect(onRunStepSpy).toHaveBeenCalled();
|
|
185
|
+
expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
|
|
186
|
+
expect(onRunStepSpy.mock.calls[0][3]).toBeDefined(); // Graph exists
|
|
187
|
+
|
|
188
|
+
const { handleLLMEnd, collected } = createMetadataAggregator();
|
|
189
|
+
const titleResult = await run.generateTitle({
|
|
190
|
+
provider,
|
|
191
|
+
inputText: userMessage,
|
|
192
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
193
|
+
contentParts,
|
|
194
|
+
clientOptions: llmConfig,
|
|
195
|
+
chainOptions: {
|
|
196
|
+
callbacks: [
|
|
197
|
+
{
|
|
198
|
+
handleLLMEnd,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(titleResult).toBeDefined();
|
|
205
|
+
expect(titleResult.title).toBeDefined();
|
|
206
|
+
expect(titleResult.language).toBeDefined();
|
|
207
|
+
expect(collected).toBeDefined();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (isContentFilterError(error)) {
|
|
210
|
+
contentFilterTriggered = true;
|
|
211
|
+
console.warn('Skipping test: Azure content filter triggered');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
201
216
|
});
|
|
202
217
|
|
|
203
218
|
test(`${capitalizeFirstLetter(provider)}: should generate title using completion method`, async () => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
219
|
+
if (contentFilterTriggered) {
|
|
220
|
+
console.warn(
|
|
221
|
+
'Skipping test: Azure content filter was triggered in previous test'
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const llmConfig = getLLMConfig(provider);
|
|
227
|
+
const customHandlers = setupCustomHandlers();
|
|
228
|
+
|
|
229
|
+
run = await Run.create<t.IState>({
|
|
230
|
+
runId: 'test-run-id-completion',
|
|
231
|
+
graphConfig: {
|
|
232
|
+
type: 'standard',
|
|
233
|
+
llmConfig,
|
|
234
|
+
tools: [new Calculator()],
|
|
235
|
+
instructions:
|
|
236
|
+
'You are a helpful AI assistant. Keep responses concise and friendly.',
|
|
237
|
+
},
|
|
238
|
+
returnContent: true,
|
|
239
|
+
customHandlers,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const userMessage = 'What can you help me with today?';
|
|
243
|
+
conversationHistory = [];
|
|
244
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
245
|
+
|
|
246
|
+
const inputs = {
|
|
247
|
+
messages: conversationHistory,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const finalContentParts = await run.processStream(inputs, config);
|
|
251
|
+
expect(finalContentParts).toBeDefined();
|
|
252
|
+
|
|
253
|
+
const { handleLLMEnd, collected } = createMetadataAggregator();
|
|
254
|
+
const titleResult = await run.generateTitle({
|
|
255
|
+
provider,
|
|
256
|
+
inputText: userMessage,
|
|
257
|
+
titleMethod: TitleMethod.COMPLETION,
|
|
258
|
+
contentParts,
|
|
259
|
+
clientOptions: llmConfig,
|
|
260
|
+
chainOptions: {
|
|
261
|
+
callbacks: [
|
|
262
|
+
{
|
|
263
|
+
handleLLMEnd,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(titleResult).toBeDefined();
|
|
270
|
+
expect(titleResult.title).toBeDefined();
|
|
271
|
+
expect(titleResult.title).not.toBe('');
|
|
272
|
+
expect(titleResult.language).toBeUndefined();
|
|
273
|
+
expect(collected).toBeDefined();
|
|
274
|
+
console.log(`Completion method generated title: "${titleResult.title}"`);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (isContentFilterError(error)) {
|
|
277
|
+
contentFilterTriggered = true;
|
|
278
|
+
console.warn('Skipping test: Azure content filter triggered');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
256
283
|
});
|
|
257
284
|
|
|
258
285
|
test(`${capitalizeFirstLetter(provider)}: should follow-up`, async () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
finalMessages
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
286
|
+
if (contentFilterTriggered || runningHistory == null) {
|
|
287
|
+
console.warn(
|
|
288
|
+
'Skipping test: Azure content filter was triggered or no conversation history'
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
console.log('Previous conversation length:', runningHistory.length);
|
|
294
|
+
console.log(
|
|
295
|
+
'Last message:',
|
|
296
|
+
runningHistory[runningHistory.length - 1].content
|
|
297
|
+
);
|
|
298
|
+
const llmConfig = getLLMConfig(provider);
|
|
299
|
+
const customHandlers = setupCustomHandlers();
|
|
300
|
+
|
|
301
|
+
run = await Run.create<t.IState>({
|
|
302
|
+
runId: 'test-run-id',
|
|
303
|
+
graphConfig: {
|
|
304
|
+
type: 'standard',
|
|
305
|
+
llmConfig,
|
|
306
|
+
tools: [new Calculator()],
|
|
307
|
+
instructions:
|
|
308
|
+
'You are a helpful AI assistant. Keep responses concise and friendly.',
|
|
309
|
+
},
|
|
310
|
+
returnContent: true,
|
|
311
|
+
customHandlers,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
conversationHistory = runningHistory.slice();
|
|
315
|
+
conversationHistory.push(new HumanMessage('What else can you tell me?'));
|
|
316
|
+
|
|
317
|
+
const inputs = {
|
|
318
|
+
messages: conversationHistory,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const finalContentParts = await run.processStream(inputs, config);
|
|
322
|
+
expect(finalContentParts).toBeDefined();
|
|
323
|
+
const allTextParts = finalContentParts?.every(
|
|
324
|
+
(part) => part.type === ContentTypes.TEXT
|
|
325
|
+
);
|
|
326
|
+
expect(allTextParts).toBe(true);
|
|
327
|
+
expect(collectedUsage.length).toBeGreaterThan(0);
|
|
328
|
+
expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
|
|
329
|
+
expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
|
|
330
|
+
|
|
331
|
+
const finalMessages = run.getRunMessages();
|
|
332
|
+
expect(finalMessages).toBeDefined();
|
|
333
|
+
expect(finalMessages?.length).toBeGreaterThan(0);
|
|
334
|
+
console.log(
|
|
335
|
+
`${capitalizeFirstLetter(provider)} follow-up message:`,
|
|
336
|
+
finalMessages?.[finalMessages.length - 1]?.content
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(onMessageDeltaSpy).toHaveBeenCalled();
|
|
340
|
+
expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
|
|
341
|
+
|
|
342
|
+
expect(onRunStepSpy).toHaveBeenCalled();
|
|
343
|
+
expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (isContentFilterError(error)) {
|
|
346
|
+
console.warn('Skipping test: Azure content filter triggered');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
312
351
|
});
|
|
313
352
|
|
|
314
353
|
test('should handle errors appropriately', async () => {
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -20,7 +20,8 @@ import type { BaseMessage, AIMessage } from '@langchain/core/messages';
|
|
|
20
20
|
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
21
21
|
import type * as t from '@/types';
|
|
22
22
|
import { RunnableCallable } from '@/utils';
|
|
23
|
-
import {
|
|
23
|
+
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
24
|
+
import { Constants, GraphEvents } from '@/common';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Helper to check if a value is a Send object
|
|
@@ -44,6 +45,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
44
45
|
private programmaticCache?: t.ProgrammaticCache;
|
|
45
46
|
/** Reference to Graph's sessions map for automatic session injection */
|
|
46
47
|
private sessions?: t.ToolSessionMap;
|
|
48
|
+
/** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
|
|
49
|
+
private eventDrivenMode: boolean = false;
|
|
50
|
+
/** Tool definitions for event-driven mode */
|
|
51
|
+
private toolDefinitions?: Map<string, t.LCTool>;
|
|
47
52
|
|
|
48
53
|
constructor({
|
|
49
54
|
tools,
|
|
@@ -56,6 +61,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
56
61
|
loadRuntimeTools,
|
|
57
62
|
toolRegistry,
|
|
58
63
|
sessions,
|
|
64
|
+
eventDrivenMode,
|
|
65
|
+
toolDefinitions,
|
|
59
66
|
}: t.ToolNodeConstructorParams) {
|
|
60
67
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
61
68
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -66,6 +73,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
66
73
|
this.toolUsageCount = new Map<string, number>();
|
|
67
74
|
this.toolRegistry = toolRegistry;
|
|
68
75
|
this.sessions = sessions;
|
|
76
|
+
this.eventDrivenMode = eventDrivenMode ?? false;
|
|
77
|
+
this.toolDefinitions = toolDefinitions;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -243,11 +252,77 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
|
|
257
|
+
* Used in event-driven mode where the host handles actual tool execution.
|
|
258
|
+
*/
|
|
259
|
+
private async executeViaEvent(
|
|
260
|
+
toolCalls: ToolCall[],
|
|
261
|
+
config: RunnableConfig,
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
|
+
input: any
|
|
264
|
+
): Promise<T> {
|
|
265
|
+
const requests: t.ToolCallRequest[] = toolCalls.map((call) => {
|
|
266
|
+
const turn = this.toolUsageCount.get(call.name) ?? 0;
|
|
267
|
+
this.toolUsageCount.set(call.name, turn + 1);
|
|
268
|
+
return {
|
|
269
|
+
id: call.id!,
|
|
270
|
+
name: call.name,
|
|
271
|
+
args: call.args as Record<string, unknown>,
|
|
272
|
+
stepId: this.toolCallStepIds?.get(call.id!),
|
|
273
|
+
turn,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const results = await new Promise<t.ToolExecuteResult[]>(
|
|
278
|
+
(resolve, reject) => {
|
|
279
|
+
const request: t.ToolExecuteBatchRequest = {
|
|
280
|
+
toolCalls: requests,
|
|
281
|
+
userId: config.configurable?.user_id as string | undefined,
|
|
282
|
+
agentId: config.configurable?.agent_id as string | undefined,
|
|
283
|
+
resolve,
|
|
284
|
+
reject,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, request, config);
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const outputs: ToolMessage[] = results.map((result) => {
|
|
292
|
+
const toolName =
|
|
293
|
+
requests.find((r) => r.id === result.toolCallId)?.name ?? 'unknown';
|
|
294
|
+
|
|
295
|
+
if (result.status === 'error') {
|
|
296
|
+
return new ToolMessage({
|
|
297
|
+
status: 'error',
|
|
298
|
+
content: `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`,
|
|
299
|
+
name: toolName,
|
|
300
|
+
tool_call_id: result.toolCallId,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return new ToolMessage({
|
|
305
|
+
status: 'success',
|
|
306
|
+
content:
|
|
307
|
+
typeof result.content === 'string'
|
|
308
|
+
? result.content
|
|
309
|
+
: JSON.stringify(result.content),
|
|
310
|
+
name: toolName,
|
|
311
|
+
tool_call_id: result.toolCallId,
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
316
|
+
}
|
|
317
|
+
|
|
246
318
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
319
|
protected async run(input: any, config: RunnableConfig): Promise<T> {
|
|
248
320
|
let outputs: (BaseMessage | Command)[];
|
|
249
321
|
|
|
250
322
|
if (this.isSendInput(input)) {
|
|
323
|
+
if (this.eventDrivenMode) {
|
|
324
|
+
return this.executeViaEvent([input.lg_tool_call], config, input);
|
|
325
|
+
}
|
|
251
326
|
outputs = [await this.runTool(input.lg_tool_call, config)];
|
|
252
327
|
} else {
|
|
253
328
|
let messages: BaseMessage[];
|
|
@@ -289,21 +364,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
289
364
|
this.programmaticCache = undefined; // Invalidate cache on toolMap change
|
|
290
365
|
}
|
|
291
366
|
|
|
367
|
+
const filteredCalls =
|
|
368
|
+
aiMessage.tool_calls?.filter((call) => {
|
|
369
|
+
/**
|
|
370
|
+
* Filter out:
|
|
371
|
+
* 1. Already processed tool calls (present in toolMessageIds)
|
|
372
|
+
* 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
|
|
373
|
+
* which are executed by the provider's API and don't require invocation
|
|
374
|
+
*/
|
|
375
|
+
return (
|
|
376
|
+
(call.id == null || !toolMessageIds.has(call.id)) &&
|
|
377
|
+
!(call.id?.startsWith('srvtoolu_') ?? false)
|
|
378
|
+
);
|
|
379
|
+
}) ?? [];
|
|
380
|
+
|
|
381
|
+
if (this.eventDrivenMode && filteredCalls.length > 0) {
|
|
382
|
+
return this.executeViaEvent(filteredCalls, config, input);
|
|
383
|
+
}
|
|
384
|
+
|
|
292
385
|
outputs = await Promise.all(
|
|
293
|
-
|
|
294
|
-
?.filter((call) => {
|
|
295
|
-
/**
|
|
296
|
-
* Filter out:
|
|
297
|
-
* 1. Already processed tool calls (present in toolMessageIds)
|
|
298
|
-
* 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
|
|
299
|
-
* which are executed by the provider's API and don't require invocation
|
|
300
|
-
*/
|
|
301
|
-
return (
|
|
302
|
-
(call.id == null || !toolMessageIds.has(call.id)) &&
|
|
303
|
-
!(call.id?.startsWith('srvtoolu_') ?? false)
|
|
304
|
-
);
|
|
305
|
-
})
|
|
306
|
-
.map((call) => this.runTool(call, config)) ?? []
|
|
386
|
+
filteredCalls.map((call) => this.runTool(call, config))
|
|
307
387
|
);
|
|
308
388
|
}
|
|
309
389
|
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* These tests hit the LIVE Code API and verify end-to-end functionality.
|
|
5
5
|
*
|
|
6
6
|
* Run with: npm test -- ProgrammaticToolCalling.integration.test.ts
|
|
7
|
+
*
|
|
8
|
+
* Requires LIBRECHAT_CODE_API_KEY environment variable.
|
|
9
|
+
* Tests are skipped when the API key is not available.
|
|
7
10
|
*/
|
|
8
11
|
import { config as dotenvConfig } from 'dotenv';
|
|
9
12
|
dotenvConfig();
|
|
@@ -19,19 +22,17 @@ import {
|
|
|
19
22
|
createProgrammaticToolRegistry,
|
|
20
23
|
} from '@/test/mockTools';
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
|
|
26
|
+
const shouldSkip = apiKey == null || apiKey === '';
|
|
27
|
+
|
|
28
|
+
const describeIfApiKey = shouldSkip ? describe.skip : describe;
|
|
29
|
+
|
|
30
|
+
describeIfApiKey('ProgrammaticToolCalling - Live API Integration', () => {
|
|
23
31
|
let ptcTool: ReturnType<typeof createProgrammaticToolCallingTool>;
|
|
24
32
|
let toolMap: t.ToolMap;
|
|
25
33
|
let toolDefinitions: t.LCTool[];
|
|
26
34
|
|
|
27
35
|
beforeAll(() => {
|
|
28
|
-
const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
|
|
29
|
-
if (apiKey == null || apiKey === '') {
|
|
30
|
-
throw new Error(
|
|
31
|
-
'LIBRECHAT_CODE_API_KEY not set. Required for integration tests.'
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
36
|
const tools = [
|
|
36
37
|
createGetTeamMembersTool(),
|
|
37
38
|
createGetExpensesTool(),
|
|
@@ -42,7 +43,7 @@ describe('ProgrammaticToolCalling - Live API Integration', () => {
|
|
|
42
43
|
toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
43
44
|
toolDefinitions = Array.from(createProgrammaticToolRegistry().values());
|
|
44
45
|
|
|
45
|
-
ptcTool = createProgrammaticToolCallingTool({ apiKey });
|
|
46
|
+
ptcTool = createProgrammaticToolCallingTool({ apiKey: apiKey! });
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
it('executes simple single tool call', async () => {
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* These tests hit the LIVE Code API and verify end-to-end search functionality.
|
|
5
5
|
*
|
|
6
6
|
* Run with: npm test -- ToolSearch.integration.test.ts
|
|
7
|
+
*
|
|
8
|
+
* Requires LIBRECHAT_CODE_API_KEY environment variable.
|
|
9
|
+
* Tests are skipped when the API key is not available.
|
|
7
10
|
*/
|
|
8
11
|
import { config as dotenvConfig } from 'dotenv';
|
|
9
12
|
dotenvConfig();
|
|
@@ -12,19 +15,17 @@ import { describe, it, expect, beforeAll } from '@jest/globals';
|
|
|
12
15
|
import { createToolSearch } from '../ToolSearch';
|
|
13
16
|
import { createToolSearchToolRegistry } from '@/test/mockTools';
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
|
|
19
|
+
const shouldSkip = apiKey == null || apiKey === '';
|
|
20
|
+
|
|
21
|
+
const describeIfApiKey = shouldSkip ? describe.skip : describe;
|
|
22
|
+
|
|
23
|
+
describeIfApiKey('ToolSearch - Live API Integration', () => {
|
|
16
24
|
let searchTool: ReturnType<typeof createToolSearch>;
|
|
17
25
|
const toolRegistry = createToolSearchToolRegistry();
|
|
18
26
|
|
|
19
27
|
beforeAll(() => {
|
|
20
|
-
|
|
21
|
-
if (apiKey == null || apiKey === '') {
|
|
22
|
-
throw new Error(
|
|
23
|
-
'LIBRECHAT_CODE_API_KEY not set. Required for integration tests.'
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
searchTool = createToolSearch({ apiKey, toolRegistry });
|
|
28
|
+
searchTool = createToolSearch({ apiKey: apiKey!, toolRegistry });
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
it('searches for expense-related tools', async () => {
|