Tavily PR Agent commited on
Commit
933d2c0
·
1 Parent(s): d3da808

feat: add Tavily as optional search backend in WebSearchTool

Browse files
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -112,6 +112,7 @@
112
  "ws": "^8.20.0",
113
  "xss": "^1.0.15",
114
  "yaml": "^2.8.3",
 
115
  "zod": "^4.3.6"
116
  },
117
  "devDependencies": {
 
112
  "ws": "^8.20.0",
113
  "xss": "^1.0.15",
114
  "yaml": "^2.8.3",
115
+ "@tavily/core": "^0.6.1",
116
  "zod": "^4.3.6"
117
  },
118
  "devDependencies": {
src/tools/WebSearchTool/WebSearchTool.ts CHANGED
@@ -4,6 +4,7 @@ import { buildTool, type ToolDef } from '../../Tool.js'
4
  import { lazySchema } from '../../utils/lazySchema.js'
5
  import { logError } from '../../utils/log.js'
6
  import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
 
7
  import {
8
  getToolUseSummary,
9
  renderToolResultMessage,
@@ -93,6 +94,38 @@ async function searchSearXNG(
93
  }
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  /**
97
  * 文本清洗
98
  */
@@ -123,7 +156,7 @@ function cleanSearchResult(result: any) {
123
 
124
  export const WebSearchTool = buildTool({
125
  name: WEB_SEARCH_TOOL_NAME,
126
- description: 'Search the web using local SearXNG',
127
  shouldDefer: true,
128
 
129
  getToolUseSummary,
@@ -201,7 +234,10 @@ export const WebSearchTool = buildTool({
201
  })
202
  }
203
 
204
- const results = await searchSearXNG(input.query)
 
 
 
205
 
206
  const cleaned = results.map(r => ({
207
  ...r,
 
4
  import { lazySchema } from '../../utils/lazySchema.js'
5
  import { logError } from '../../utils/log.js'
6
  import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
7
+ import { tavily } from '@tavily/core'
8
  import {
9
  getToolUseSummary,
10
  renderToolResultMessage,
 
94
  }
95
  }
96
 
97
+ /**
98
+ * 使用 Tavily 云搜索
99
+ */
100
+ async function searchTavily(
101
+ query: string
102
+ ): Promise<Array<{ title: string; url: string; snippet?: string }>> {
103
+ try {
104
+ const apiKey = process.env.TAVILY_API_KEY
105
+ if (!apiKey) {
106
+ throw new Error('TAVILY_API_KEY is not set')
107
+ }
108
+
109
+ const client = tavily({ apiKey })
110
+ const response = await client.search(query, {
111
+ maxResults: 10,
112
+ searchDepth: 'basic',
113
+ topic: 'general',
114
+ })
115
+
116
+ return (response.results || []).map((r: any) => ({
117
+ title: r.title,
118
+ url: r.url,
119
+ snippet: r.content,
120
+ }))
121
+ } catch (error) {
122
+ logError('Tavily search failed', error)
123
+ throw new Error(
124
+ `Tavily search failed: ${error instanceof Error ? error.message : String(error)}`
125
+ )
126
+ }
127
+ }
128
+
129
  /**
130
  * 文本清洗
131
  */
 
156
 
157
  export const WebSearchTool = buildTool({
158
  name: WEB_SEARCH_TOOL_NAME,
159
+ description: 'Search the web using local SearXNG or Tavily (when TAVILY_API_KEY is set)',
160
  shouldDefer: true,
161
 
162
  getToolUseSummary,
 
234
  })
235
  }
236
 
237
+ const useTavily = !!process.env.TAVILY_API_KEY
238
+ const results = useTavily
239
+ ? await searchTavily(input.query)
240
+ : await searchSearXNG(input.query)
241
 
242
  const cleaned = results.map(r => ({
243
  ...r,
src/tools/WebSearchTool/__tests__/WebSearchTool.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { test, expect, describe } from 'bun:test'
2
  import { WebSearchTool } from '../WebSearchTool'
3
 
4
  describe('WebSearchTool', () => {
@@ -224,4 +224,60 @@ describe('WebSearchTool', () => {
224
  expect(result.data.durationSeconds).toBeGreaterThan(0)
225
  }, 60000)
226
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  })
 
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test'
2
  import { WebSearchTool } from '../WebSearchTool'
3
 
4
  describe('WebSearchTool', () => {
 
224
  expect(result.data.durationSeconds).toBeGreaterThan(0)
225
  }, 60000)
226
  })
227
+
228
+ describe('Tavily Integration', () => {
229
+ let originalTavilyKey: string | undefined
230
+
231
+ beforeEach(() => {
232
+ originalTavilyKey = process.env.TAVILY_API_KEY
233
+ })
234
+
235
+ afterEach(() => {
236
+ if (originalTavilyKey !== undefined) {
237
+ process.env.TAVILY_API_KEY = originalTavilyKey
238
+ } else {
239
+ delete process.env.TAVILY_API_KEY
240
+ }
241
+ })
242
+
243
+ test('should use SearXNG when TAVILY_API_KEY is not set', async () => {
244
+ delete process.env.TAVILY_API_KEY
245
+
246
+ const result = await WebSearchTool.call(
247
+ { query: 'test query' },
248
+ {},
249
+ () => {},
250
+ null
251
+ )
252
+
253
+ expect(result).toBeDefined()
254
+ expect(result.data.query).toBe('test query')
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
  })
src/tools/WebSearchTool/prompt.ts CHANGED
@@ -6,14 +6,15 @@ export function getWebSearchPrompt(): string {
6
  const currentMonthYear = getLocalMonthYear()
7
 
8
  return `
9
- - Allows VersperClaw to search the web using a local SearXNG search engine 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
- - Uses SearXNG metasearch engine (aggregates multiple sources)
 
17
  - Results may vary in quality; prioritize relevance and credibility
18
  - If results are weak or empty, try rephrasing the query
19
  - Prefer more specific queries when possible (add keywords, version numbers, or context)
 
6
  const currentMonthYear = getLocalMonthYear()
7
 
8
  return `
9
+ - Allows VersperClaw 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)