chenbhao Claude Opus 4.6 commited on
Commit
398a15b
Β·
1 Parent(s): 1f6ed64

refactor(WebSearchTool): split Tavily for general search, SearXNG for image search

Browse files

Tavily lacks image search support, so route search_images requests
to SearXNG unconditionally and use Tavily only for general queries.
Update tests to cover image search and the new routing logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

src/tools/WebSearchTool/WebSearchTool.ts CHANGED
@@ -167,7 +167,7 @@ function cleanSearchResult(result: any) {
167
 
168
  export const WebSearchTool = buildTool({
169
  name: WEB_SEARCH_TOOL_NAME,
170
- description: 'Search the web using local SearXNG or Tavily (when TAVILY_API_KEY is set)',
171
  shouldDefer: true,
172
 
173
  getToolUseSummary,
@@ -245,10 +245,12 @@ export const WebSearchTool = buildTool({
245
  })
246
  }
247
 
248
- const useTavily = !!process.env.TAVILY_API_KEY
249
- const results = useTavily
250
- ? await searchTavily(input.query)
251
- : await searchSearXNG(input.query, input.search_images)
 
 
252
 
253
  const cleaned = results.map(r => ({
254
  ...r,
 
167
 
168
  export const WebSearchTool = buildTool({
169
  name: WEB_SEARCH_TOOL_NAME,
170
+ description: 'Search the web β€” Tavily (when TAVILY_API_KEY is set) for general search, SearXNG for image search',
171
  shouldDefer: true,
172
 
173
  getToolUseSummary,
 
245
  })
246
  }
247
 
248
+ // Tavily εͺεšι€šη”¨ζœη΄’οΌŒSearXNG εͺεšε›Ύη‰‡ζœη΄’
249
+ const results = input.search_images
250
+ ? await searchSearXNG(input.query, true)
251
+ : process.env.TAVILY_API_KEY
252
+ ? await searchTavily(input.query)
253
+ : await searchSearXNG(input.query, false)
254
 
255
  const cleaned = results.map(r => ({
256
  ...r,
src/tools/WebSearchTool/__tests__/WebSearchTool.test.ts CHANGED
@@ -190,7 +190,9 @@ describe('WebSearchTool', () => {
190
  )
191
 
192
  expect(result.type).toBe('tool_result')
193
- expect(typeof result.content).toBe('string')
 
 
194
  })
195
 
196
  test('should handle empty results', () => {
@@ -205,7 +207,7 @@ describe('WebSearchTool', () => {
205
  'id'
206
  )
207
 
208
- expect(result.content).toContain('Results')
209
  })
210
  })
211
 
@@ -223,6 +225,35 @@ describe('WebSearchTool', () => {
223
  expect(Array.isArray(result.data.results)).toBe(true)
224
  expect(result.data.durationSeconds).toBeGreaterThan(0)
225
  }, 60000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  })
227
 
228
  describe('Tavily Integration', () => {
@@ -255,29 +286,29 @@ describe('WebSearchTool', () => {
255
  expect(Array.isArray(result.data.results)).toBe(true)
256
  }, 60000)
257
 
258
- test('should use Tavily when TAVILY_API_KEY is set', async () => {
259
  process.env.TAVILY_API_KEY = 'tvly-test-key'
260
 
261
  const result = await WebSearchTool.call(
262
- { query: 'test tavily query' },
263
  {},
264
  () => {},
265
  null
266
  )
267
 
268
- // With a fake key, Tavily will fail and we should get an error result
269
  expect(result).toBeDefined()
270
- expect(result.data.query).toBe('test tavily query')
271
  expect(Array.isArray(result.data.results)).toBe(true)
272
- // The result should contain an error since the API key is invalid
273
  const hasError = result.data.results.some(
274
  r => typeof r === 'string' && r.includes('Error')
275
  )
276
  expect(hasError).toBe(true)
277
  }, 60000)
278
 
279
- test('should include Tavily in description when key is set', () => {
280
  expect(WebSearchTool.description).toContain('Tavily')
 
281
  })
282
  })
283
  })
 
190
  )
191
 
192
  expect(result.type).toBe('tool_result')
193
+ expect(Array.isArray(result.content)).toBe(true)
194
+ expect(result.content[0].type).toBe('text')
195
+ expect(typeof result.content[0].text).toBe('string')
196
  })
197
 
198
  test('should handle empty results', () => {
 
207
  'id'
208
  )
209
 
210
+ expect(result.content[0].text).toContain('Results')
211
  })
212
  })
213
 
 
225
  expect(Array.isArray(result.data.results)).toBe(true)
226
  expect(result.data.durationSeconds).toBeGreaterThan(0)
227
  }, 60000)
228
+
229
+ test('should return image URLs with search_images: true', async () => {
230
+ const result = await WebSearchTool.call(
231
+ { query: 'milet ε†™ηœŸ', search_images: true },
232
+ {},
233
+ () => {},
234
+ null
235
+ )
236
+
237
+ expect(result).toBeDefined()
238
+ expect(result.data.query).toBe('milet ε†™ηœŸ')
239
+
240
+ // Extract and print all image URLs from results
241
+ const imageUrls: string[] = []
242
+ for (const r of result.data.results) {
243
+ if (typeof r === 'string') continue
244
+ for (const item of r.content) {
245
+ if (item.image) imageUrls.push(item.image)
246
+ }
247
+ }
248
+
249
+ console.log(`\nFound ${imageUrls.length} image URLs for "milet ε†™ηœŸ":`)
250
+ for (const url of imageUrls) {
251
+ console.log(` ${url}`)
252
+ }
253
+
254
+ // Results may be empty if engines are slow, but the tool should not crash
255
+ expect(Array.isArray(result.data.results)).toBe(true)
256
+ }, 60000)
257
  })
258
 
259
  describe('Tavily Integration', () => {
 
286
  expect(Array.isArray(result.data.results)).toBe(true)
287
  }, 60000)
288
 
289
+ test('should return error when Tavily API key is invalid', async () => {
290
  process.env.TAVILY_API_KEY = 'tvly-test-key'
291
 
292
  const result = await WebSearchTool.call(
293
+ { query: 'typescript' },
294
  {},
295
  () => {},
296
  null
297
  )
298
 
 
299
  expect(result).toBeDefined()
300
+ expect(result.data.query).toBe('typescript')
301
  expect(Array.isArray(result.data.results)).toBe(true)
302
+ // With an invalid Tavily key, searchTavily throws β†’ outer catch returns an error
303
  const hasError = result.data.results.some(
304
  r => typeof r === 'string' && r.includes('Error')
305
  )
306
  expect(hasError).toBe(true)
307
  }, 60000)
308
 
309
+ test('should mention Tavily and SearXNG in description', () => {
310
  expect(WebSearchTool.description).toContain('Tavily')
311
+ expect(WebSearchTool.description).toContain('SearXNG')
312
  })
313
  })
314
  })
src/tools/WebSearchTool/prompt.ts CHANGED
@@ -6,24 +6,20 @@ export function getWebSearchPrompt(): string {
6
  const currentMonthYear = getLocalMonthYear()
7
 
8
  return `
9
- - Allows Codev to search the web using a local SearXNG search engine or Tavily cloud search (when TAVILY_API_KEY is configured) and use the results to inform responses
10
  - Provides up-to-date information for current events, technical documentation, and recent data
11
  - Returns structured search results including titles, URLs, and snippets
12
  - Works with all AI providers including local models
13
  - Designed for high-recall search to support reasoning and retrieval-augmented generation (RAG)
14
 
15
- Search Strategy:
16
- - Default: Uses SearXNG metasearch engine (aggregates multiple sources)
17
- - When TAVILY_API_KEY is set: Uses Tavily cloud search API for high-quality, LLM-optimized results
18
- - Results may vary in quality; prioritize relevance and credibility
19
- - If results are weak or empty, try rephrasing the query
20
- - Prefer more specific queries when possible (add keywords, version numbers, or context)
21
 
22
- Image Search (search_images parameter):
23
  - Set **search_images: true** to search specifically for images (photos, artworks, screenshots).
24
  Example: WebSearch(query: "milet ε†™ηœŸ", search_images: true)
25
- - When search_images is true, results include direct image URLs (img_src) that you can display
26
- with ImageShowTool(src: img_src).
27
  - Example workflow: WebSearch(query: "...", search_images: true) β†’ pick a result β†’ ImageShowTool(src: img_src)
28
  - Without search_images: true, this is a general web search and image URLs are NOT available.
29
  - DO NOT pass article/page URLs to ImageShowTool β€” it only accepts direct image URLs (.jpg/.png/.gif/.webp).
 
6
  const currentMonthYear = getLocalMonthYear()
7
 
8
  return `
9
+ - Allows Codev to search the web and use results to inform responses
10
  - Provides up-to-date information for current events, technical documentation, and recent data
11
  - Returns structured search results including titles, URLs, and snippets
12
  - Works with all AI providers including local models
13
  - Designed for high-recall search to support reasoning and retrieval-augmented generation (RAG)
14
 
15
+ Search backends (automatic β€” no action needed):
16
+ - **General web search**: Uses Tavily cloud API when TAVILY_API_KEY is set; falls back to SearXNG if not
17
+ - **Image search** (search_images: true): Always uses SearXNG (Tavily does not support image search)
 
 
 
18
 
19
+ Image Search (search_images: true):
20
  - Set **search_images: true** to search specifically for images (photos, artworks, screenshots).
21
  Example: WebSearch(query: "milet ε†™ηœŸ", search_images: true)
22
+ - Results include direct image URLs (img_src) that you can display with ImageShowTool(src: img_src).
 
23
  - Example workflow: WebSearch(query: "...", search_images: true) β†’ pick a result β†’ ImageShowTool(src: img_src)
24
  - Without search_images: true, this is a general web search and image URLs are NOT available.
25
  - DO NOT pass article/page URLs to ImageShowTool β€” it only accepts direct image URLs (.jpg/.png/.gif/.webp).