@librechat/agents 2.4.320 → 2.4.322

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.
Files changed (45) hide show
  1. package/dist/cjs/tools/search/firecrawl.cjs +6 -4
  2. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  3. package/dist/cjs/tools/search/format.cjs +117 -80
  4. package/dist/cjs/tools/search/format.cjs.map +1 -1
  5. package/dist/cjs/tools/search/rerankers.cjs +43 -36
  6. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  7. package/dist/cjs/tools/search/schema.cjs +70 -0
  8. package/dist/cjs/tools/search/schema.cjs.map +1 -0
  9. package/dist/cjs/tools/search/search.cjs +125 -52
  10. package/dist/cjs/tools/search/search.cjs.map +1 -1
  11. package/dist/cjs/tools/search/tool.cjs +162 -47
  12. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  13. package/dist/cjs/tools/search/utils.cjs +34 -5
  14. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  15. package/dist/esm/tools/search/firecrawl.mjs +6 -4
  16. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  17. package/dist/esm/tools/search/format.mjs +118 -81
  18. package/dist/esm/tools/search/format.mjs.map +1 -1
  19. package/dist/esm/tools/search/rerankers.mjs +43 -36
  20. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  21. package/dist/esm/tools/search/schema.mjs +61 -0
  22. package/dist/esm/tools/search/schema.mjs.map +1 -0
  23. package/dist/esm/tools/search/search.mjs +126 -53
  24. package/dist/esm/tools/search/search.mjs.map +1 -1
  25. package/dist/esm/tools/search/tool.mjs +161 -46
  26. package/dist/esm/tools/search/tool.mjs.map +1 -1
  27. package/dist/esm/tools/search/utils.mjs +33 -6
  28. package/dist/esm/tools/search/utils.mjs.map +1 -1
  29. package/dist/types/tools/search/firecrawl.d.ts +1 -0
  30. package/dist/types/tools/search/rerankers.d.ts +8 -4
  31. package/dist/types/tools/search/schema.d.ts +16 -0
  32. package/dist/types/tools/search/tool.d.ts +13 -0
  33. package/dist/types/tools/search/types.d.ts +36 -0
  34. package/dist/types/tools/search/utils.d.ts +9 -2
  35. package/package.json +3 -2
  36. package/src/scripts/search.ts +3 -0
  37. package/src/tools/search/firecrawl.ts +9 -4
  38. package/src/tools/search/format.ts +157 -87
  39. package/src/tools/search/rerankers.ts +57 -36
  40. package/src/tools/search/schema.ts +63 -0
  41. package/src/tools/search/search.ts +165 -52
  42. package/src/tools/search/tool.ts +217 -44
  43. package/src/tools/search/types.ts +37 -0
  44. package/src/tools/search/utils.ts +37 -5
  45. package/src/utils/llmConfig.ts +1 -1
@@ -1,88 +1,246 @@
1
- /* eslint-disable no-console */
2
1
  import { z } from 'zod';
3
2
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
4
3
  import type { RunnableConfig } from '@langchain/core/runnables';
5
4
  import type * as t from './types';
5
+ import {
6
+ DATE_RANGE,
7
+ querySchema,
8
+ dateSchema,
9
+ countrySchema,
10
+ imagesSchema,
11
+ videosSchema,
12
+ newsSchema,
13
+ } from './schema';
6
14
  import { createSearchAPI, createSourceProcessor } from './search';
7
15
  import { createFirecrawlScraper } from './firecrawl';
8
16
  import { expandHighlights } from './highlights';
9
17
  import { formatResultsForLLM } from './format';
18
+ import { createDefaultLogger } from './utils';
10
19
  import { createReranker } from './rerankers';
11
20
  import { Constants } from '@/common';
12
21
 
13
- const DEFAULT_QUERY_DESCRIPTION = `
14
- GUIDELINES:
15
- - Start broad, then narrow: Begin with key concepts, then refine with specifics
16
- - Think like sources: Use terminology experts would use in the field
17
- - Consider perspective: Frame queries from different viewpoints for better results
18
- - Quality over quantity: A precise 3-4 word query often beats lengthy sentences
19
-
20
- TECHNIQUES (combine for power searches):
21
- - EXACT PHRASES: Use quotes ("climate change report")
22
- - EXCLUDE TERMS: Use minus to remove unwanted results (-wikipedia)
23
- - SITE-SPECIFIC: Restrict to websites (site:edu research)
24
- - FILETYPE: Find specific documents (filetype:pdf study)
25
- - OR OPERATOR: Find alternatives (electric OR hybrid cars)
26
- - DATE RANGE: Recent information (data after:2020)
27
- - WILDCARDS: Use * for unknown terms (how to * bread)
28
- - SPECIFIC QUESTIONS: Use who/what/when/where/why/how
29
- - DOMAIN TERMS: Include technical terminology for specialized topics
30
- - CONCISE TERMS: Prioritize keywords over sentences
31
- `.trim();
32
-
33
- const DEFAULT_COUNTRY_DESCRIPTION = `Country code to localize search results.
34
- Use standard 2-letter country codes: "us", "uk", "ca", "de", "fr", "jp", "br", etc.
35
- Provide this when the search should return results specific to a particular country.
36
- Examples:
37
- - "us" for United States (default)
38
- - "de" for Germany
39
- - "in" for India
40
- `.trim();
22
+ /**
23
+ * Executes parallel searches and merges the results
24
+ */
25
+ async function executeParallelSearches({
26
+ searchAPI,
27
+ query,
28
+ date,
29
+ country,
30
+ safeSearch,
31
+ images,
32
+ videos,
33
+ news,
34
+ logger,
35
+ }: {
36
+ searchAPI: ReturnType<typeof createSearchAPI>;
37
+ query: string;
38
+ date?: DATE_RANGE;
39
+ country?: string;
40
+ safeSearch: t.SearchToolConfig['safeSearch'];
41
+ images: boolean;
42
+ videos: boolean;
43
+ news: boolean;
44
+ logger: t.Logger;
45
+ }): Promise<t.SearchResult> {
46
+ // Prepare all search tasks to run in parallel
47
+ const searchTasks: Promise<t.SearchResult>[] = [
48
+ // Main search
49
+ searchAPI.getSources({
50
+ query,
51
+ date,
52
+ country,
53
+ safeSearch,
54
+ }),
55
+ ];
56
+
57
+ if (images) {
58
+ searchTasks.push(
59
+ searchAPI
60
+ .getSources({
61
+ query,
62
+ date,
63
+ country,
64
+ safeSearch,
65
+ type: 'images',
66
+ })
67
+ .catch((error) => {
68
+ logger.error('Error fetching images:', error);
69
+ return {
70
+ success: false,
71
+ error: `Images search failed: ${error instanceof Error ? error.message : String(error)}`,
72
+ };
73
+ })
74
+ );
75
+ }
76
+ if (videos) {
77
+ searchTasks.push(
78
+ searchAPI
79
+ .getSources({
80
+ query,
81
+ date,
82
+ country,
83
+ safeSearch,
84
+ type: 'videos',
85
+ })
86
+ .catch((error) => {
87
+ logger.error('Error fetching videos:', error);
88
+ return {
89
+ success: false,
90
+ error: `Videos search failed: ${error instanceof Error ? error.message : String(error)}`,
91
+ };
92
+ })
93
+ );
94
+ }
95
+ if (news) {
96
+ searchTasks.push(
97
+ searchAPI
98
+ .getSources({
99
+ query,
100
+ date,
101
+ country,
102
+ safeSearch,
103
+ type: 'news',
104
+ })
105
+ .catch((error) => {
106
+ logger.error('Error fetching news:', error);
107
+ return {
108
+ success: false,
109
+ error: `News search failed: ${error instanceof Error ? error.message : String(error)}`,
110
+ };
111
+ })
112
+ );
113
+ }
114
+
115
+ // Run all searches in parallel
116
+ const results = await Promise.all(searchTasks);
117
+
118
+ // Get the main search result (first result)
119
+ const mainResult = results[0];
120
+ if (!mainResult.success) {
121
+ throw new Error(mainResult.error ?? 'Search failed');
122
+ }
123
+
124
+ // Merge additional results with the main results
125
+ const mergedResults = { ...mainResult.data };
126
+
127
+ // Convert existing news to topStories if present
128
+ if (mergedResults.news !== undefined && mergedResults.news.length > 0) {
129
+ const existingNewsAsTopStories = mergedResults.news
130
+ .filter((newsItem) => newsItem.link !== undefined && newsItem.link !== '')
131
+ .map((newsItem) => ({
132
+ title: newsItem.title ?? '',
133
+ link: newsItem.link ?? '',
134
+ source: newsItem.source ?? '',
135
+ date: newsItem.date ?? '',
136
+ imageUrl: newsItem.imageUrl ?? '',
137
+ processed: false,
138
+ }));
139
+ mergedResults.topStories = [
140
+ ...(mergedResults.topStories ?? []),
141
+ ...existingNewsAsTopStories,
142
+ ];
143
+ delete mergedResults.news;
144
+ }
145
+
146
+ results.slice(1).forEach((result) => {
147
+ if (result.success && result.data !== undefined) {
148
+ if (result.data.images !== undefined && result.data.images.length > 0) {
149
+ mergedResults.images = [
150
+ ...(mergedResults.images ?? []),
151
+ ...result.data.images,
152
+ ];
153
+ }
154
+ if (result.data.videos !== undefined && result.data.videos.length > 0) {
155
+ mergedResults.videos = [
156
+ ...(mergedResults.videos ?? []),
157
+ ...result.data.videos,
158
+ ];
159
+ }
160
+ if (result.data.news !== undefined && result.data.news.length > 0) {
161
+ const newsAsTopStories = result.data.news.map((newsItem) => ({
162
+ ...newsItem,
163
+ link: newsItem.link ?? '',
164
+ }));
165
+ mergedResults.topStories = [
166
+ ...(mergedResults.topStories ?? []),
167
+ ...newsAsTopStories,
168
+ ];
169
+ }
170
+ }
171
+ });
172
+
173
+ return { success: true, data: mergedResults };
174
+ }
41
175
 
42
176
  function createSearchProcessor({
43
177
  searchAPI,
178
+ safeSearch,
44
179
  sourceProcessor,
45
180
  onGetHighlights,
181
+ logger,
46
182
  }: {
183
+ safeSearch: t.SearchToolConfig['safeSearch'];
47
184
  searchAPI: ReturnType<typeof createSearchAPI>;
48
185
  sourceProcessor: ReturnType<typeof createSourceProcessor>;
49
186
  onGetHighlights: t.SearchToolConfig['onGetHighlights'];
187
+ logger: t.Logger;
50
188
  }) {
51
189
  return async function ({
52
190
  query,
191
+ date,
53
192
  country,
54
193
  proMode = true,
55
194
  maxSources = 5,
56
195
  onSearchResults,
196
+ images = false,
197
+ videos = false,
198
+ news = false,
57
199
  }: {
58
200
  query: string;
59
201
  country?: string;
60
- maxSources?: number;
202
+ date?: DATE_RANGE;
61
203
  proMode?: boolean;
204
+ maxSources?: number;
62
205
  onSearchResults: t.SearchToolConfig['onSearchResults'];
206
+ images?: boolean;
207
+ videos?: boolean;
208
+ news?: boolean;
63
209
  }): Promise<t.SearchResultData> {
64
210
  try {
65
- const result = await searchAPI.getSources({ query, country });
66
- onSearchResults?.(result);
211
+ // Execute parallel searches and merge results
212
+ const searchResult = await executeParallelSearches({
213
+ searchAPI,
214
+ query,
215
+ date,
216
+ country,
217
+ safeSearch,
218
+ images,
219
+ videos,
220
+ news,
221
+ logger,
222
+ });
67
223
 
68
- if (!result.success) {
69
- throw new Error(result.error ?? 'Search failed');
70
- }
224
+ onSearchResults?.(searchResult);
71
225
 
72
226
  const processedSources = await sourceProcessor.processSources({
73
227
  query,
74
- result,
228
+ news,
229
+ result: searchResult,
75
230
  proMode,
76
231
  onGetHighlights,
77
232
  numElements: maxSources,
78
233
  });
234
+
79
235
  return expandHighlights(processedSources);
80
236
  } catch (error) {
81
- console.error('Error in search:', error);
237
+ logger.error('Error in search:', error);
82
238
  return {
83
239
  organic: [],
84
240
  topStories: [],
85
241
  images: [],
242
+ videos: [],
243
+ news: [],
86
244
  relatedSearches: [],
87
245
  error: error instanceof Error ? error.message : String(error),
88
246
  };
@@ -116,11 +274,15 @@ function createTool({
116
274
  }): DynamicStructuredTool<typeof schema> {
117
275
  return tool<typeof schema>(
118
276
  async (params, runnableConfig) => {
119
- const { query, country: _c } = params;
277
+ const { query, date, country: _c, images, videos, news } = params;
120
278
  const country = typeof _c === 'string' && _c ? _c : undefined;
121
279
  const searchResult = await search({
122
280
  query,
281
+ date,
123
282
  country,
283
+ images,
284
+ videos,
285
+ news,
124
286
  onSearchResults: createOnSearchResults({
125
287
  runnableConfig,
126
288
  onSearchResults: _onSearchResults,
@@ -181,6 +343,7 @@ export const createSearchTool = (
181
343
  topResults = 5,
182
344
  strategies = ['no_extraction'],
183
345
  filterContent = true,
346
+ safeSearch = 1,
184
347
  firecrawlApiKey,
185
348
  firecrawlApiUrl,
186
349
  firecrawlFormats = ['markdown', 'html'],
@@ -190,19 +353,25 @@ export const createSearchTool = (
190
353
  onGetHighlights,
191
354
  } = config;
192
355
 
193
- const querySchema = z.string().describe(DEFAULT_QUERY_DESCRIPTION);
356
+ const logger = config.logger || createDefaultLogger();
357
+
194
358
  const schemaObject: {
195
359
  query: z.ZodString;
360
+ date: z.ZodOptional<z.ZodNativeEnum<typeof DATE_RANGE>>;
196
361
  country?: z.ZodOptional<z.ZodString>;
362
+ images: z.ZodOptional<z.ZodBoolean>;
363
+ videos: z.ZodOptional<z.ZodBoolean>;
364
+ news: z.ZodOptional<z.ZodBoolean>;
197
365
  } = {
198
366
  query: querySchema,
367
+ date: dateSchema,
368
+ images: imagesSchema,
369
+ videos: videosSchema,
370
+ news: newsSchema,
199
371
  };
200
372
 
201
373
  if (searchProvider === 'serper') {
202
- schemaObject.country = z
203
- .string()
204
- .optional()
205
- .describe(DEFAULT_COUNTRY_DESCRIPTION);
374
+ schemaObject.country = countrySchema;
206
375
  }
207
376
 
208
377
  const toolSchema = z.object(schemaObject);
@@ -224,10 +393,11 @@ export const createSearchTool = (
224
393
  rerankerType,
225
394
  jinaApiKey,
226
395
  cohereApiKey,
396
+ logger,
227
397
  });
228
398
 
229
399
  if (!selectedReranker) {
230
- console.warn('No reranker selected. Using default ranking.');
400
+ logger.warn('No reranker selected. Using default ranking.');
231
401
  }
232
402
 
233
403
  const sourceProcessor = createSourceProcessor(
@@ -236,14 +406,17 @@ export const createSearchTool = (
236
406
  topResults,
237
407
  strategies,
238
408
  filterContent,
409
+ logger,
239
410
  },
240
411
  firecrawlScraper
241
412
  );
242
413
 
243
414
  const search = createSearchProcessor({
244
415
  searchAPI,
416
+ safeSearch,
245
417
  sourceProcessor,
246
418
  onGetHighlights,
419
+ logger,
247
420
  });
248
421
 
249
422
  return createTool({
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import type { Logger as WinstonLogger } from 'winston';
2
3
  import type { RunnableConfig } from '@langchain/core/runnables';
3
4
  import type { BaseReranker } from './rerankers';
5
+ import { DATE_RANGE } from './schema';
4
6
 
5
7
  export type SearchProvider = 'serper' | 'searxng';
6
8
  export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
@@ -16,6 +18,7 @@ export type ProcessedSource = {
16
18
  attribution?: string;
17
19
  references?: References;
18
20
  highlights?: Highlight[];
21
+ processed?: boolean;
19
22
  };
20
23
 
21
24
  export type ProcessedOrganic = OrganicResult & ProcessedSource;
@@ -24,6 +27,7 @@ export type ValidSource = ProcessedOrganic | ProcessedTopStory;
24
27
 
25
28
  export type ResultReference = {
26
29
  link: string;
30
+ type: 'link' | 'image' | 'video';
27
31
  title?: string;
28
32
  attribution?: string;
29
33
  };
@@ -84,6 +88,7 @@ export interface ProcessSourcesConfig {
84
88
  strategies?: string[];
85
89
  filterContent?: boolean;
86
90
  reranker?: BaseReranker;
91
+ logger?: Logger;
87
92
  }
88
93
 
89
94
  export interface FirecrawlConfig {
@@ -133,10 +138,15 @@ export interface CohereRerankerResponse {
133
138
  };
134
139
  }
135
140
 
141
+ export type SafeSearchLevel = 0 | 1 | 2;
142
+
143
+ export type Logger = WinstonLogger;
136
144
  export interface SearchToolConfig
137
145
  extends SearchConfig,
138
146
  ProcessSourcesConfig,
139
147
  FirecrawlConfig {
148
+ logger?: Logger;
149
+ safeSearch?: SafeSearchLevel;
140
150
  jinaApiKey?: string;
141
151
  cohereApiKey?: string;
142
152
  rerankerType?: RerankerType;
@@ -249,12 +259,19 @@ export interface FirecrawlScraperConfig {
249
259
  apiUrl?: string;
250
260
  formats?: string[];
251
261
  timeout?: number;
262
+ logger?: Logger;
252
263
  }
253
264
 
254
265
  export type GetSourcesParams = {
255
266
  query: string;
267
+ date?: DATE_RANGE;
256
268
  country?: string;
257
269
  numResults?: number;
270
+ safeSearch?: SearchToolConfig['safeSearch'];
271
+ images?: boolean;
272
+ videos?: boolean;
273
+ news?: boolean;
274
+ type?: 'search' | 'images' | 'videos' | 'news';
258
275
  };
259
276
 
260
277
  /** Serper API */
@@ -433,6 +450,13 @@ export interface SerperSearchInput {
433
450
  */
434
451
  autocorrect?: boolean;
435
452
  page?: number;
453
+ /**
454
+ * Date range for search results
455
+ * Options: "h" (past hour), "d" (past 24 hours), "w" (past week),
456
+ * "m" (past month), "y" (past year)
457
+ * `qdr:${DATE_RANGE}`
458
+ */
459
+ tbs?: string;
436
460
  }
437
461
 
438
462
  export type SerperResultData = {
@@ -562,6 +586,7 @@ export type ProcessSourcesFields = {
562
586
  result: SearchResult;
563
587
  numElements: number;
564
588
  query: string;
589
+ news: boolean;
565
590
  proMode: boolean;
566
591
  onGetHighlights: SearchToolConfig['onGetHighlights'];
567
592
  };
@@ -569,16 +594,28 @@ export type ProcessSourcesFields = {
569
594
  export type SearchToolSchema = z.ZodObject<
570
595
  {
571
596
  query: z.ZodString;
597
+ date: z.ZodOptional<z.ZodNativeEnum<typeof DATE_RANGE>>;
572
598
  country?: z.ZodOptional<z.ZodString>;
599
+ images: z.ZodOptional<z.ZodBoolean>;
600
+ videos: z.ZodOptional<z.ZodBoolean>;
601
+ news: z.ZodOptional<z.ZodBoolean>;
573
602
  },
574
603
  'strip',
575
604
  z.ZodTypeAny,
576
605
  {
577
606
  query: string;
607
+ date?: DATE_RANGE;
578
608
  country?: unknown;
609
+ images?: boolean;
610
+ videos?: boolean;
611
+ news?: boolean;
579
612
  },
580
613
  {
581
614
  query: string;
615
+ date?: DATE_RANGE;
582
616
  country?: unknown;
617
+ images?: boolean;
618
+ videos?: boolean;
619
+ news?: boolean;
583
620
  }
584
621
  >;
@@ -1,9 +1,36 @@
1
1
  /* eslint-disable no-console */
2
+
2
3
  import type * as t from './types';
3
4
 
5
+ /**
6
+ * Singleton instance of the default logger
7
+ */
8
+ let defaultLoggerInstance: t.Logger | null = null;
9
+
10
+ /**
11
+ * Creates a default logger that maps to console methods
12
+ * Uses a singleton pattern to avoid creating multiple instances
13
+ * @returns A default logger that implements the Logger interface
14
+ */
15
+ export const createDefaultLogger = (): t.Logger => {
16
+ if (!defaultLoggerInstance) {
17
+ defaultLoggerInstance = {
18
+ error: console.error,
19
+ warn: console.warn,
20
+ info: console.info,
21
+ debug: console.debug,
22
+ } as t.Logger;
23
+ }
24
+ return defaultLoggerInstance;
25
+ };
26
+
27
+ export const fileExtRegex =
28
+ /\.(pdf|jpe?g|png|gif|svg|webp|bmp|ico|tiff?|avif|heic|doc[xm]?|xls[xm]?|ppt[xm]?|zip|rar|mp[34]|mov|avi|wav)$/i;
29
+
4
30
  export const getDomainName = (
5
31
  link: string,
6
- metadata?: t.ScrapeMetadata
32
+ metadata?: t.ScrapeMetadata,
33
+ logger?: t.Logger
7
34
  ): string | undefined => {
8
35
  try {
9
36
  const url = metadata?.sourceURL ?? metadata?.url ?? (link || '');
@@ -13,7 +40,11 @@ export const getDomainName = (
13
40
  }
14
41
  } catch (e) {
15
42
  // URL parsing failed
16
- console.error('Error parsing URL:', e);
43
+ if (logger) {
44
+ logger.error('Error parsing URL:', e);
45
+ } else {
46
+ console.error('Error parsing URL:', e);
47
+ }
17
48
  }
18
49
 
19
50
  return;
@@ -21,9 +52,10 @@ export const getDomainName = (
21
52
 
22
53
  export function getAttribution(
23
54
  link: string,
24
- metadata?: t.ScrapeMetadata
55
+ metadata?: t.ScrapeMetadata,
56
+ logger?: t.Logger
25
57
  ): string | undefined {
26
- if (!metadata) return getDomainName(link, metadata);
58
+ if (!metadata) return getDomainName(link, metadata, logger);
27
59
 
28
60
  const twitterSite = metadata['twitter:site'];
29
61
  const twitterSiteFormatted =
@@ -43,5 +75,5 @@ export function getAttribution(
43
75
  return attribution;
44
76
  }
45
77
 
46
- return getDomainName(link, metadata);
78
+ return getDomainName(link, metadata, logger);
47
79
  }
@@ -6,7 +6,7 @@ import type * as t from '@/types';
6
6
  export const llmConfigs: Record<string, t.LLMConfig | undefined> = {
7
7
  [Providers.OPENAI]: {
8
8
  provider: Providers.OPENAI,
9
- model: 'gpt-4o-mini',
9
+ model: 'gpt-4.1',
10
10
  temperature: 0.7,
11
11
  streaming: true,
12
12
  streamUsage: true,