@librechat/agents 2.4.321 → 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 +6 -0
  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 +62 -25
  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 +7 -1
  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 +63 -26
  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 +34 -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 -3
  37. package/src/tools/search/firecrawl.ts +9 -4
  38. package/src/tools/search/format.ts +8 -1
  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 +74 -22
  42. package/src/tools/search/tool.ts +217 -44
  43. package/src/tools/search/types.ts +35 -0
  44. package/src/tools/search/utils.ts +37 -5
  45. package/src/utils/llmConfig.ts +1 -1
@@ -1,10 +1,9 @@
1
- /* eslint-disable no-console */
2
1
  import axios from 'axios';
3
2
  import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
4
3
  import type * as t from './types';
4
+ import { getAttribution, createDefaultLogger } from './utils';
5
5
  import { FirecrawlScraper } from './firecrawl';
6
6
  import { BaseReranker } from './rerankers';
7
- import { getAttribution } from './utils';
8
7
 
9
8
  const chunker = {
10
9
  cleanText: (text: string): string => {
@@ -52,12 +51,14 @@ const chunker = {
52
51
  chunkSize?: number;
53
52
  chunkOverlap?: number;
54
53
  separators?: string[];
55
- }
54
+ },
55
+ logger?: t.Logger
56
56
  ): Promise<string[][]> => {
57
57
  // Split multiple texts
58
+ const logger_ = logger || createDefaultLogger();
58
59
  const promises = texts.map((text) =>
59
60
  chunker.splitText(text, options).catch((error) => {
60
- console.error('Error splitting text:', error);
61
+ logger_.error('Error splitting text:', error);
61
62
  return [text];
62
63
  })
63
64
  );
@@ -82,18 +83,22 @@ const getHighlights = async ({
82
83
  content,
83
84
  reranker,
84
85
  topResults = 5,
86
+ logger,
85
87
  }: {
86
88
  content: string;
87
89
  query: string;
88
90
  reranker?: BaseReranker;
89
91
  topResults?: number;
92
+ logger?: t.Logger;
90
93
  }): Promise<t.Highlight[] | undefined> => {
94
+ const logger_ = logger || createDefaultLogger();
95
+
91
96
  if (!content) {
92
- console.warn('No content provided for highlights');
97
+ logger_.warn('No content provided for highlights');
93
98
  return;
94
99
  }
95
100
  if (!reranker) {
96
- console.warn('No reranker provided for highlights');
101
+ logger_.warn('No reranker provided for highlights');
97
102
  return;
98
103
  }
99
104
 
@@ -102,14 +107,14 @@ const getHighlights = async ({
102
107
  if (Array.isArray(documents)) {
103
108
  return await reranker.rerank(query, documents, topResults);
104
109
  } else {
105
- console.error(
110
+ logger_.error(
106
111
  'Expected documents to be an array, got:',
107
112
  typeof documents
108
113
  );
109
114
  return;
110
115
  }
111
116
  } catch (error) {
112
- console.error('Error in content processing:', error);
117
+ logger_.error('Error in content processing:', error);
113
118
  return;
114
119
  }
115
120
  };
@@ -131,25 +136,49 @@ const createSerperAPI = (
131
136
 
132
137
  const getSources = async ({
133
138
  query,
139
+ date,
134
140
  country,
141
+ safeSearch,
135
142
  numResults = 8,
143
+ type,
136
144
  }: t.GetSourcesParams): Promise<t.SearchResult> => {
137
145
  if (!query.trim()) {
138
146
  return { success: false, error: 'Query cannot be empty' };
139
147
  }
140
148
 
141
149
  try {
150
+ const safe = ['off', 'moderate', 'active'] as const;
142
151
  const payload: t.SerperSearchPayload = {
143
152
  q: query,
153
+ safe: safe[safeSearch ?? 1],
144
154
  num: Math.min(Math.max(1, numResults), 10),
145
155
  };
146
156
 
157
+ // Set the search type if provided
158
+ if (type) {
159
+ payload.type = type;
160
+ }
161
+
162
+ if (date != null) {
163
+ payload.tbs = `qdr:${date}`;
164
+ }
165
+
147
166
  if (country != null && country !== '') {
148
167
  payload['gl'] = country.toLowerCase();
149
168
  }
150
169
 
170
+ // Determine the API endpoint based on the search type
171
+ let apiEndpoint = config.apiUrl;
172
+ if (type === 'images') {
173
+ apiEndpoint = 'https://google.serper.dev/images';
174
+ } else if (type === 'videos') {
175
+ apiEndpoint = 'https://google.serper.dev/videos';
176
+ } else if (type === 'news') {
177
+ apiEndpoint = 'https://google.serper.dev/news';
178
+ }
179
+
151
180
  const response = await axios.post<t.SerperResultData>(
152
- config.apiUrl,
181
+ apiEndpoint,
153
182
  payload,
154
183
  {
155
184
  headers: {
@@ -169,6 +198,8 @@ const createSerperAPI = (
169
198
  peopleAlsoAsk: data.peopleAlsoAsk,
170
199
  knowledgeGraph: data.knowledgeGraph,
171
200
  relatedSearches: data.relatedSearches,
201
+ videos: data.videos ?? [],
202
+ news: data.news ?? [],
172
203
  };
173
204
 
174
205
  return { success: true, data: results };
@@ -202,6 +233,7 @@ const createSearXNGAPI = (
202
233
  const getSources = async ({
203
234
  query,
204
235
  numResults = 8,
236
+ type,
205
237
  }: t.GetSourcesParams): Promise<t.SearchResult> => {
206
238
  if (!query.trim()) {
207
239
  return { success: false, error: 'Query cannot be empty' };
@@ -218,12 +250,22 @@ const createSearXNGAPI = (
218
250
  searchUrl = searchUrl.replace(/\/$/, '') + '/search';
219
251
  }
220
252
 
253
+ // Determine the search category based on the type
254
+ let category = 'general';
255
+ if (type === 'images') {
256
+ category = 'images';
257
+ } else if (type === 'videos') {
258
+ category = 'videos';
259
+ } else if (type === 'news') {
260
+ category = 'news';
261
+ }
262
+
221
263
  // Prepare parameters for SearXNG
222
264
  const params: t.SearxNGSearchPayload = {
223
265
  q: query,
224
266
  format: 'json',
225
267
  pageno: 1,
226
- categories: 'general',
268
+ categories: category,
227
269
  language: 'all',
228
270
  safesearch: 0,
229
271
  engines: 'google,bing,duckduckgo',
@@ -271,6 +313,8 @@ const createSearXNGAPI = (
271
313
  topStories: [],
272
314
  // Use undefined instead of null for optional properties
273
315
  relatedSearches: data.suggestions ?? [],
316
+ videos: [],
317
+ news: [],
274
318
  };
275
319
 
276
320
  return { success: true, data: results };
@@ -327,8 +371,10 @@ export const createSourceProcessor = (
327
371
  // strategies = ['no_extraction'],
328
372
  // filterContent = true,
329
373
  reranker,
374
+ logger,
330
375
  } = config;
331
376
 
377
+ const logger_ = logger || createDefaultLogger();
332
378
  const firecrawlScraper = scraperInstance;
333
379
 
334
380
  const webScraper = {
@@ -341,7 +387,7 @@ export const createSourceProcessor = (
341
387
  links: string[];
342
388
  onGetHighlights: t.SearchToolConfig['onGetHighlights'];
343
389
  }): Promise<Array<t.ScrapeResult>> => {
344
- console.log(`Scraping ${links.length} links with Firecrawl`);
390
+ logger_.debug(`Scraping ${links.length} links with Firecrawl`);
345
391
  const promises: Array<Promise<t.ScrapeResult>> = [];
346
392
  try {
347
393
  for (let i = 0; i < links.length; i++) {
@@ -349,7 +395,11 @@ export const createSourceProcessor = (
349
395
  const promise: Promise<t.ScrapeResult> = firecrawlScraper
350
396
  .scrapeUrl(currentLink, {})
351
397
  .then(([url, response]) => {
352
- const attribution = getAttribution(url, response.data?.metadata);
398
+ const attribution = getAttribution(
399
+ url,
400
+ response.data?.metadata,
401
+ logger_
402
+ );
353
403
  if (response.success && response.data) {
354
404
  const [content, references] =
355
405
  firecrawlScraper.extractContent(response);
@@ -371,8 +421,9 @@ export const createSourceProcessor = (
371
421
  .then(async (result) => {
372
422
  try {
373
423
  if (result.error != null) {
374
- console.error(
375
- `Error scraping ${result.url}: ${result.content}`
424
+ logger_.error(
425
+ `Error scraping ${result.url}: ${result.content}`,
426
+ result.error
376
427
  );
377
428
  return {
378
429
  ...result,
@@ -382,6 +433,7 @@ export const createSourceProcessor = (
382
433
  query,
383
434
  reranker,
384
435
  content: result.content,
436
+ logger: logger_,
385
437
  });
386
438
  if (onGetHighlights) {
387
439
  onGetHighlights(result.url);
@@ -391,14 +443,14 @@ export const createSourceProcessor = (
391
443
  highlights,
392
444
  };
393
445
  } catch (error) {
394
- console.error('Error processing scraped content:', error);
446
+ logger_.error('Error processing scraped content:', error);
395
447
  return {
396
448
  ...result,
397
449
  };
398
450
  }
399
451
  })
400
452
  .catch((error) => {
401
- console.error(`Error scraping ${currentLink}:`, error);
453
+ logger_.error(`Error scraping ${currentLink}:`, error);
402
454
  return {
403
455
  url: currentLink,
404
456
  error: true,
@@ -409,7 +461,7 @@ export const createSourceProcessor = (
409
461
  }
410
462
  return await Promise.all(promises);
411
463
  } catch (error) {
412
- console.error('Error in scrapeMany:', error);
464
+ logger_.error('Error in scrapeMany:', error);
413
465
  return [];
414
466
  }
415
467
  },
@@ -453,6 +505,7 @@ export const createSourceProcessor = (
453
505
  result,
454
506
  numElements,
455
507
  query,
508
+ news,
456
509
  proMode = true,
457
510
  onGetHighlights,
458
511
  }: t.ProcessSourcesFields): Promise<t.SearchResultData> => {
@@ -520,7 +573,7 @@ export const createSourceProcessor = (
520
573
  organicLinksSet
521
574
  );
522
575
 
523
- if (organicLinks.length === 0 && topStoryLinks.length === 0) {
576
+ if (organicLinks.length === 0 && (topStoryLinks.length === 0 || !news)) {
524
577
  return result.data;
525
578
  }
526
579
 
@@ -541,7 +594,7 @@ export const createSourceProcessor = (
541
594
  }
542
595
 
543
596
  // Process top story links
544
- if (topStoryLinks.length > 0) {
597
+ if (news && topStoryLinks.length > 0) {
545
598
  promises.push(
546
599
  fetchContents({
547
600
  query,
@@ -555,18 +608,17 @@ export const createSourceProcessor = (
555
608
 
556
609
  await Promise.all(promises);
557
610
 
558
- // Update sources with scraped content
559
611
  if (result.data.organic.length > 0) {
560
612
  updateSourcesWithContent(result.data.organic, sourceMap);
561
613
  }
562
614
 
563
- if (topStories.length > 0) {
615
+ if (news && topStories.length > 0) {
564
616
  updateSourcesWithContent(topStories, sourceMap);
565
617
  }
566
618
 
567
619
  return result.data;
568
620
  } catch (error) {
569
- console.error('Error in processSources:', error);
621
+ logger_.error('Error in processSources:', error);
570
622
  return {
571
623
  organic: [],
572
624
  topStories: [],
@@ -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({