@librechat/agents 3.1.1 → 3.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.01",
3
+ "version": "3.1.2",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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
- const { userName, location } = await getArgs();
133
- const llmConfig = getLLMConfig(provider);
134
- const customHandlers = setupCustomHandlers();
135
-
136
- run = await Run.create<t.IState>({
137
- runId: 'test-run-id',
138
- graphConfig: {
139
- type: 'standard',
140
- llmConfig,
141
- tools: [new Calculator()],
142
- instructions:
143
- 'You are a friendly AI assistant. Always address the user by their name.',
144
- additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
145
- },
146
- returnContent: true,
147
- customHandlers,
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
- expect(titleResult).toBeDefined();
198
- expect(titleResult.title).toBeDefined();
199
- expect(titleResult.language).toBeDefined();
200
- expect(collected).toBeDefined();
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
- const { userName, location } = await getArgs();
205
- const llmConfig = getLLMConfig(provider);
206
- const customHandlers = setupCustomHandlers();
207
-
208
- run = await Run.create<t.IState>({
209
- runId: 'test-run-id-completion',
210
- graphConfig: {
211
- type: 'standard',
212
- llmConfig,
213
- tools: [new Calculator()],
214
- instructions:
215
- 'You are a friendly AI assistant. Always address the user by their name.',
216
- additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
217
- },
218
- returnContent: true,
219
- customHandlers,
220
- });
221
-
222
- const userMessage = 'What is the weather like today?';
223
- conversationHistory = [];
224
- conversationHistory.push(new HumanMessage(userMessage));
225
-
226
- const inputs = {
227
- messages: conversationHistory,
228
- };
229
-
230
- const finalContentParts = await run.processStream(inputs, config);
231
- expect(finalContentParts).toBeDefined();
232
-
233
- const { handleLLMEnd, collected } = createMetadataAggregator();
234
- const titleResult = await run.generateTitle({
235
- provider,
236
- inputText: userMessage,
237
- titleMethod: TitleMethod.COMPLETION, // Using completion method
238
- contentParts,
239
- clientOptions: llmConfig,
240
- chainOptions: {
241
- callbacks: [
242
- {
243
- handleLLMEnd,
244
- },
245
- ],
246
- },
247
- });
248
-
249
- expect(titleResult).toBeDefined();
250
- expect(titleResult.title).toBeDefined();
251
- expect(titleResult.title).not.toBe('');
252
- // Completion method doesn't return language
253
- expect(titleResult.language).toBeUndefined();
254
- expect(collected).toBeDefined();
255
- console.log(`Completion method generated title: "${titleResult.title}"`);
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
- console.log('Previous conversation length:', runningHistory.length);
260
- console.log(
261
- 'Last message:',
262
- runningHistory[runningHistory.length - 1].content
263
- );
264
- const { userName, location } = await getArgs();
265
- const llmConfig = getLLMConfig(provider);
266
- const customHandlers = setupCustomHandlers();
267
-
268
- run = await Run.create<t.IState>({
269
- runId: 'test-run-id',
270
- graphConfig: {
271
- type: 'standard',
272
- llmConfig,
273
- tools: [new Calculator()],
274
- instructions:
275
- 'You are a friendly AI assistant. Always address the user by their name.',
276
- additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
277
- },
278
- returnContent: true,
279
- customHandlers,
280
- });
281
-
282
- conversationHistory = runningHistory.slice();
283
- conversationHistory.push(new HumanMessage('how are you?'));
284
-
285
- const inputs = {
286
- messages: conversationHistory,
287
- };
288
-
289
- const finalContentParts = await run.processStream(inputs, config);
290
- expect(finalContentParts).toBeDefined();
291
- const allTextParts = finalContentParts?.every(
292
- (part) => part.type === ContentTypes.TEXT
293
- );
294
- expect(allTextParts).toBe(true);
295
- expect(collectedUsage.length).toBeGreaterThan(0);
296
- expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
297
- expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
298
-
299
- const finalMessages = run.getRunMessages();
300
- expect(finalMessages).toBeDefined();
301
- expect(finalMessages?.length).toBeGreaterThan(0);
302
- console.log(
303
- `${capitalizeFirstLetter(provider)} follow-up message:`,
304
- finalMessages?.[finalMessages.length - 1]?.content
305
- );
306
-
307
- expect(onMessageDeltaSpy).toHaveBeenCalled();
308
- expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
309
-
310
- expect(onRunStepSpy).toHaveBeenCalled();
311
- expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
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 () => {
@@ -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
- describe('ProgrammaticToolCalling - Live API Integration', () => {
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
- describe('ToolSearch - Live API Integration', () => {
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
- const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
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 () => {