Merge pull request #6 from Tavily-FDE/feat/tavily-migration/websearchtool-searxng-additive-tavily
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
|
|
|
|
|
|
|
|
|
|
| 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)
|