mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: add web_search docs
This commit is contained in:
@@ -1,43 +1,165 @@
|
|||||||
# Web Search Tool (`web_search`)
|
# Web Search Tool (`web_search`)
|
||||||
|
|
||||||
This document describes the `web_search` tool.
|
This document describes the `web_search` tool for performing web searches using multiple providers.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Use `web_search` to perform a web search using the Tavily API. The tool returns a concise answer with sources when possible.
|
Use `web_search` to perform a web search and get information from the internet. The tool supports multiple search providers and returns a concise answer with source citations when available.
|
||||||
|
|
||||||
|
### Supported Providers
|
||||||
|
|
||||||
|
1. **DashScope** (Official, Free) - Default provider, always available when using Qwen OAuth authentication
|
||||||
|
2. **Tavily** - High-quality search API with built-in answer generation
|
||||||
|
3. **Google Custom Search** - Google's Custom Search JSON API
|
||||||
|
|
||||||
### Arguments
|
### Arguments
|
||||||
|
|
||||||
`web_search` takes one argument:
|
`web_search` takes two arguments:
|
||||||
|
|
||||||
- `query` (string, required): The search query.
|
- `query` (string, required): The search query
|
||||||
|
- `provider` (string, optional): Specific provider to use ("dashscope", "tavily", "google")
|
||||||
|
- If not specified, uses the default provider from configuration
|
||||||
|
|
||||||
## How to use `web_search`
|
## Configuration
|
||||||
|
|
||||||
`web_search` calls the Tavily API directly. You must configure the `TAVILY_API_KEY` through one of the following methods:
|
### Method 1: Settings File (Recommended)
|
||||||
|
|
||||||
1. **Settings file**: Add `"tavilyApiKey": "your-key-here"` to your `settings.json`
|
Add to your `settings.json`:
|
||||||
2. **Environment variable**: Set `TAVILY_API_KEY` in your environment or `.env` file
|
|
||||||
3. **Command line**: Use `--tavily-api-key your-key-here` when running the CLI
|
|
||||||
|
|
||||||
If the key is not configured, the tool will be disabled and skipped.
|
```json
|
||||||
|
{
|
||||||
Usage:
|
"webSearch": {
|
||||||
|
"provider": [
|
||||||
```
|
{ "type": "dashscope" },
|
||||||
web_search(query="Your query goes here.")
|
{ "type": "tavily", "apiKey": "tvly-xxxxx" },
|
||||||
|
{
|
||||||
|
"type": "google",
|
||||||
|
"apiKey": "your-google-api-key",
|
||||||
|
"searchEngineId": "your-search-engine-id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "dashscope"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## `web_search` examples
|
**Notes:**
|
||||||
|
|
||||||
Get information on a topic:
|
- DashScope doesn't require an API key (official, free service)
|
||||||
|
- Only configure the providers you want to use
|
||||||
|
- Set `default` to specify which provider to use by default
|
||||||
|
|
||||||
```
|
### Method 2: Environment Variables
|
||||||
web_search(query="latest advancements in AI-powered code generation")
|
|
||||||
|
Set environment variables in your shell or `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tavily
|
||||||
|
export TAVILY_API_KEY="tvly-xxxxx"
|
||||||
|
|
||||||
|
# Google
|
||||||
|
export GOOGLE_API_KEY="your-api-key"
|
||||||
|
export GOOGLE_SEARCH_ENGINE_ID="your-engine-id"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Important notes
|
### Method 3: Command Line Arguments
|
||||||
|
|
||||||
- **Response returned:** The `web_search` tool returns a concise answer when available, with a list of source links.
|
Pass API keys when running Qwen Code:
|
||||||
- **Citations:** Source links are appended as a numbered list.
|
|
||||||
- **API key:** Configure `TAVILY_API_KEY` via settings.json, environment variables, .env files, or command line arguments. If not configured, the tool is not registered.
|
```bash
|
||||||
|
# Tavily
|
||||||
|
qwen --tavily-api-key tvly-xxxxx
|
||||||
|
|
||||||
|
# Google
|
||||||
|
qwen --google-api-key your-key --google-search-engine-id your-id
|
||||||
|
|
||||||
|
# Specify default provider
|
||||||
|
qwen --web-search-default tavily
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backward Compatibility (Deprecated)
|
||||||
|
|
||||||
|
⚠️ **DEPRECATED:** The legacy `tavilyApiKey` configuration is still supported for backward compatibility but is deprecated:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"advanced": {
|
||||||
|
"tavilyApiKey": "tvly-xxxxx" // ⚠️ Deprecated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** This configuration is deprecated and will be removed in a future version. Please migrate to the new `webSearch` configuration format shown above. The old configuration will automatically configure Tavily as a provider, but we strongly recommend updating your configuration.
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic search (using default provider)
|
||||||
|
|
||||||
|
```
|
||||||
|
web_search(query="latest advancements in AI")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search with specific provider
|
||||||
|
|
||||||
|
```
|
||||||
|
web_search(query="latest advancements in AI", provider="tavily")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-world examples
|
||||||
|
|
||||||
|
```
|
||||||
|
web_search(query="weather in San Francisco today")
|
||||||
|
web_search(query="latest Node.js LTS version", provider="google")
|
||||||
|
web_search(query="best practices for React 19", provider="dashscope")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provider Details
|
||||||
|
|
||||||
|
### DashScope (Official)
|
||||||
|
|
||||||
|
- **Cost:** Free
|
||||||
|
- **Authentication:** Automatically available with Qwen OAuth
|
||||||
|
- **Configuration:** No API key required
|
||||||
|
- **Best for:** General queries, always available
|
||||||
|
|
||||||
|
### Tavily
|
||||||
|
|
||||||
|
- **Cost:** Requires API key (paid service with free tier)
|
||||||
|
- **Sign up:** https://tavily.com
|
||||||
|
- **Features:** High-quality results with AI-generated answers
|
||||||
|
- **Best for:** Research, comprehensive answers with citations
|
||||||
|
|
||||||
|
### Google Custom Search
|
||||||
|
|
||||||
|
- **Cost:** Free tier available (100 queries/day)
|
||||||
|
- **Setup:**
|
||||||
|
1. Enable Custom Search API in Google Cloud Console
|
||||||
|
2. Create a Custom Search Engine at https://programmablesearchengine.google.com
|
||||||
|
- **Features:** Google's search quality
|
||||||
|
- **Best for:** Specific, factual queries
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Response format:** Returns a concise answer with numbered source citations
|
||||||
|
- **Citations:** Source links are appended as a numbered list: [1], [2], etc.
|
||||||
|
- **Multiple providers:** If one provider fails, manually specify another using the `provider` parameter
|
||||||
|
- **DashScope availability:** Always available when authenticated with Qwen OAuth, no configuration needed
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Tool not available?**
|
||||||
|
|
||||||
|
- Check if at least one provider is configured
|
||||||
|
- For DashScope: Ensure you're authenticated with Qwen OAuth
|
||||||
|
- For Tavily/Google: Verify your API keys are correct
|
||||||
|
|
||||||
|
**Provider-specific errors?**
|
||||||
|
|
||||||
|
- Use the `provider` parameter to try a different search provider
|
||||||
|
- Check your API quotas and rate limits
|
||||||
|
- Verify API keys are properly set in configuration
|
||||||
|
|
||||||
|
**Need help?**
|
||||||
|
|
||||||
|
- Check your configuration: Run `qwen` and use the settings dialog
|
||||||
|
- View your current settings in `~/.qwen-code/settings.json` (macOS/Linux) or `%USERPROFILE%\.qwen-code\settings.json` (Windows)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { mcpCommand } from '../commands/mcp.js';
|
|||||||
|
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||||
|
import { buildWebSearchConfig } from './webSearch.js';
|
||||||
|
|
||||||
// Simple console logger for now - replace with actual logger if available
|
// Simple console logger for now - replace with actual logger if available
|
||||||
const logger = {
|
const logger = {
|
||||||
@@ -117,6 +118,9 @@ export interface CliArgs {
|
|||||||
proxy: string | undefined;
|
proxy: string | undefined;
|
||||||
includeDirectories: string[] | undefined;
|
includeDirectories: string[] | undefined;
|
||||||
tavilyApiKey: string | undefined;
|
tavilyApiKey: string | undefined;
|
||||||
|
googleApiKey: string | undefined;
|
||||||
|
googleSearchEngineId: string | undefined;
|
||||||
|
webSearchDefault: string | undefined;
|
||||||
screenReader: boolean | undefined;
|
screenReader: boolean | undefined;
|
||||||
vlmSwitchMode: string | undefined;
|
vlmSwitchMode: string | undefined;
|
||||||
useSmartEdit: boolean | undefined;
|
useSmartEdit: boolean | undefined;
|
||||||
@@ -325,7 +329,20 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
|||||||
})
|
})
|
||||||
.option('tavily-api-key', {
|
.option('tavily-api-key', {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Tavily API key for web search functionality',
|
description: 'Tavily API key for web search',
|
||||||
|
})
|
||||||
|
.option('google-api-key', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Google Custom Search API key',
|
||||||
|
})
|
||||||
|
.option('google-search-engine-id', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Google Custom Search Engine ID',
|
||||||
|
})
|
||||||
|
.option('web-search-default', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Default web search provider (dashscope, tavily, google)',
|
||||||
})
|
})
|
||||||
.option('screen-reader', {
|
.option('screen-reader', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -747,10 +764,7 @@ export async function loadCliConfig(
|
|||||||
: argv.openaiLogging) ?? false,
|
: argv.openaiLogging) ?? false,
|
||||||
},
|
},
|
||||||
cliVersion: await getCliVersion(),
|
cliVersion: await getCliVersion(),
|
||||||
tavilyApiKey:
|
webSearch: buildWebSearchConfig(argv, settings),
|
||||||
argv.tavilyApiKey ||
|
|
||||||
settings.advanced?.tavilyApiKey ||
|
|
||||||
process.env['TAVILY_API_KEY'],
|
|
||||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||||
ideMode,
|
ideMode,
|
||||||
chatCompression: settings.model?.chatCompression,
|
chatCompression: settings.model?.chatCompression,
|
||||||
@@ -771,7 +785,6 @@ export async function loadCliConfig(
|
|||||||
output: {
|
output: {
|
||||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||||
},
|
},
|
||||||
webSearch: settings.webSearch,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1044,12 +1044,12 @@ const SETTINGS_SCHEMA = {
|
|||||||
},
|
},
|
||||||
tavilyApiKey: {
|
tavilyApiKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
label: 'Tavily API Key',
|
label: 'Tavily API Key (Deprecated)',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
default: undefined as string | undefined,
|
default: undefined as string | undefined,
|
||||||
description:
|
description:
|
||||||
'The API key for the Tavily API. Required to enable the web_search tool functionality.',
|
'⚠️ DEPRECATED: Please use webSearch.provider configuration instead. Legacy API key for the Tavily API.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
91
packages/cli/src/config/webSearch.ts
Normal file
91
packages/cli/src/config/webSearch.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { WebSearchProviderConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
import type { Settings } from './settings.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI arguments related to web search configuration
|
||||||
|
*/
|
||||||
|
export interface WebSearchCliArgs {
|
||||||
|
tavilyApiKey?: string;
|
||||||
|
googleApiKey?: string;
|
||||||
|
googleSearchEngineId?: string;
|
||||||
|
webSearchDefault?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web search configuration structure
|
||||||
|
*/
|
||||||
|
export interface WebSearchConfig {
|
||||||
|
provider: WebSearchProviderConfig[];
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build webSearch configuration from multiple sources with priority:
|
||||||
|
* 1. settings.json (new format) - highest priority
|
||||||
|
* 2. Command line args + environment variables
|
||||||
|
* 3. Legacy tavilyApiKey (backward compatibility)
|
||||||
|
*
|
||||||
|
* @param argv - Command line arguments
|
||||||
|
* @param settings - User settings from settings.json
|
||||||
|
* @returns WebSearch configuration or undefined if no providers available
|
||||||
|
*/
|
||||||
|
export function buildWebSearchConfig(
|
||||||
|
argv: WebSearchCliArgs,
|
||||||
|
settings: Settings,
|
||||||
|
): WebSearchConfig | undefined {
|
||||||
|
// Priority 1: Use settings.json webSearch config if present
|
||||||
|
if (settings.webSearch) {
|
||||||
|
return settings.webSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Build from command line args and environment variables
|
||||||
|
const providers: WebSearchProviderConfig[] = [];
|
||||||
|
|
||||||
|
// DashScope is always available (official, free)
|
||||||
|
providers.push({ type: 'dashscope' } as WebSearchProviderConfig);
|
||||||
|
|
||||||
|
// Tavily from args/env/legacy settings
|
||||||
|
const tavilyKey =
|
||||||
|
argv.tavilyApiKey ||
|
||||||
|
settings.advanced?.tavilyApiKey ||
|
||||||
|
process.env['TAVILY_API_KEY'];
|
||||||
|
if (tavilyKey) {
|
||||||
|
providers.push({
|
||||||
|
type: 'tavily',
|
||||||
|
apiKey: tavilyKey,
|
||||||
|
} as WebSearchProviderConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google from args/env
|
||||||
|
const googleKey = argv.googleApiKey || process.env['GOOGLE_API_KEY'];
|
||||||
|
const googleEngineId =
|
||||||
|
argv.googleSearchEngineId || process.env['GOOGLE_SEARCH_ENGINE_ID'];
|
||||||
|
if (googleKey && googleEngineId) {
|
||||||
|
providers.push({
|
||||||
|
type: 'google',
|
||||||
|
apiKey: googleKey,
|
||||||
|
searchEngineId: googleEngineId,
|
||||||
|
} as WebSearchProviderConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no providers configured, return undefined
|
||||||
|
if (providers.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine default provider
|
||||||
|
// Priority: CLI arg > has Tavily key > DashScope (fallback)
|
||||||
|
const defaultProvider =
|
||||||
|
argv.webSearchDefault || (tavilyKey ? 'tavily' : 'dashscope');
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: providers,
|
||||||
|
default: defaultProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -330,6 +330,9 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||||||
proxy: undefined,
|
proxy: undefined,
|
||||||
includeDirectories: undefined,
|
includeDirectories: undefined,
|
||||||
tavilyApiKey: undefined,
|
tavilyApiKey: undefined,
|
||||||
|
googleApiKey: undefined,
|
||||||
|
googleSearchEngineId: undefined,
|
||||||
|
webSearchDefault: undefined,
|
||||||
screenReader: undefined,
|
screenReader: undefined,
|
||||||
vlmSwitchMode: undefined,
|
vlmSwitchMode: undefined,
|
||||||
useSmartEdit: undefined,
|
useSmartEdit: undefined,
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ export interface ConfigParameters {
|
|||||||
cliVersion?: string;
|
cliVersion?: string;
|
||||||
loadMemoryFromIncludeDirectories?: boolean;
|
loadMemoryFromIncludeDirectories?: boolean;
|
||||||
// Web search providers
|
// Web search providers
|
||||||
tavilyApiKey?: string;
|
|
||||||
webSearch?: {
|
webSearch?: {
|
||||||
provider: Array<{
|
provider: Array<{
|
||||||
type: 'tavily' | 'google' | 'dashscope';
|
type: 'tavily' | 'google' | 'dashscope';
|
||||||
@@ -356,7 +355,6 @@ export class Config {
|
|||||||
private readonly cliVersion?: string;
|
private readonly cliVersion?: string;
|
||||||
private readonly experimentalZedIntegration: boolean = false;
|
private readonly experimentalZedIntegration: boolean = false;
|
||||||
private readonly loadMemoryFromIncludeDirectories: boolean = false;
|
private readonly loadMemoryFromIncludeDirectories: boolean = false;
|
||||||
private readonly tavilyApiKey?: string;
|
|
||||||
private readonly webSearch?: {
|
private readonly webSearch?: {
|
||||||
provider: Array<{
|
provider: Array<{
|
||||||
type: 'tavily' | 'google' | 'dashscope';
|
type: 'tavily' | 'google' | 'dashscope';
|
||||||
@@ -469,7 +467,6 @@ export class Config {
|
|||||||
this.skipLoopDetection = params.skipLoopDetection ?? false;
|
this.skipLoopDetection = params.skipLoopDetection ?? false;
|
||||||
|
|
||||||
// Web search
|
// Web search
|
||||||
this.tavilyApiKey = params.tavilyApiKey;
|
|
||||||
this.webSearch = params.webSearch;
|
this.webSearch = params.webSearch;
|
||||||
this.useRipgrep = params.useRipgrep ?? true;
|
this.useRipgrep = params.useRipgrep ?? true;
|
||||||
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
||||||
@@ -904,10 +901,6 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Web search provider configuration
|
// Web search provider configuration
|
||||||
getTavilyApiKey(): string | undefined {
|
|
||||||
return this.tavilyApiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWebSearchConfig() {
|
getWebSearchConfig() {
|
||||||
return this.webSearch;
|
return this.webSearch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
||||||
import { WebSearchTool, type WebSearchToolParams } from './web-search.js';
|
|
||||||
import type { Config } from '../config/config.js';
|
|
||||||
import { GeminiClient } from '../core/client.js';
|
|
||||||
|
|
||||||
// Mock GeminiClient and Config constructor
|
|
||||||
vi.mock('../core/client.js');
|
|
||||||
vi.mock('../config/config.js');
|
|
||||||
|
|
||||||
// Mock global fetch
|
|
||||||
const mockFetch = vi.fn();
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
describe('WebSearchTool', () => {
|
|
||||||
const abortSignal = new AbortController().signal;
|
|
||||||
let mockGeminiClient: GeminiClient;
|
|
||||||
let tool: WebSearchTool;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
const mockConfigInstance = {
|
|
||||||
getGeminiClient: () => mockGeminiClient,
|
|
||||||
getProxy: () => undefined,
|
|
||||||
getTavilyApiKey: () => 'test-api-key', // Add the missing method
|
|
||||||
} as unknown as Config;
|
|
||||||
mockGeminiClient = new GeminiClient(mockConfigInstance);
|
|
||||||
tool = new WebSearchTool(mockConfigInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('build', () => {
|
|
||||||
it('should return an invocation for a valid query', () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'test query' };
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
expect(invocation).toBeDefined();
|
|
||||||
expect(invocation.params).toEqual(params);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error for an empty query', () => {
|
|
||||||
const params: WebSearchToolParams = { query: '' };
|
|
||||||
expect(() => tool.build(params)).toThrow(
|
|
||||||
"The 'query' parameter cannot be empty.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error for a query with only whitespace', () => {
|
|
||||||
const params: WebSearchToolParams = { query: ' ' };
|
|
||||||
expect(() => tool.build(params)).toThrow(
|
|
||||||
"The 'query' parameter cannot be empty.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getDescription', () => {
|
|
||||||
it('should return a description of the search', () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'test query' };
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
expect(invocation.getDescription()).toBe(
|
|
||||||
'Searching the web for: "test query"',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should return search results for a successful query', async () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'successful query' };
|
|
||||||
|
|
||||||
// Mock the fetch response
|
|
||||||
mockFetch.mockResolvedValueOnce({
|
|
||||||
ok: true,
|
|
||||||
json: async () => ({
|
|
||||||
answer: 'Here are your results.',
|
|
||||||
results: [],
|
|
||||||
}),
|
|
||||||
} as Response);
|
|
||||||
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
const result = await invocation.execute(abortSignal);
|
|
||||||
|
|
||||||
expect(result.llmContent).toBe(
|
|
||||||
'Web search results for "successful query":\n\nHere are your results.',
|
|
||||||
);
|
|
||||||
expect(result.returnDisplay).toBe(
|
|
||||||
'Search results for "successful query" returned.',
|
|
||||||
);
|
|
||||||
expect(result.sources).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle no search results found', async () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'no results query' };
|
|
||||||
|
|
||||||
// Mock the fetch response
|
|
||||||
mockFetch.mockResolvedValueOnce({
|
|
||||||
ok: true,
|
|
||||||
json: async () => ({
|
|
||||||
answer: '',
|
|
||||||
results: [],
|
|
||||||
}),
|
|
||||||
} as Response);
|
|
||||||
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
const result = await invocation.execute(abortSignal);
|
|
||||||
|
|
||||||
expect(result.llmContent).toBe(
|
|
||||||
'No search results or information found for query: "no results query"',
|
|
||||||
);
|
|
||||||
expect(result.returnDisplay).toBe('No information found.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle API errors gracefully', async () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'error query' };
|
|
||||||
|
|
||||||
// Mock the fetch to reject
|
|
||||||
mockFetch.mockRejectedValueOnce(new Error('API Failure'));
|
|
||||||
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
const result = await invocation.execute(abortSignal);
|
|
||||||
|
|
||||||
expect(result.llmContent).toContain('Error:');
|
|
||||||
expect(result.llmContent).toContain('API Failure');
|
|
||||||
expect(result.returnDisplay).toBe('Error performing web search.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly format results with sources', async () => {
|
|
||||||
const params: WebSearchToolParams = { query: 'grounding query' };
|
|
||||||
|
|
||||||
// Mock the fetch response
|
|
||||||
mockFetch.mockResolvedValueOnce({
|
|
||||||
ok: true,
|
|
||||||
json: async () => ({
|
|
||||||
answer: 'This is a test response.',
|
|
||||||
results: [
|
|
||||||
{ title: 'Example Site', url: 'https://example.com' },
|
|
||||||
{ title: 'Google', url: 'https://google.com' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
} as Response);
|
|
||||||
|
|
||||||
const invocation = tool.build(params);
|
|
||||||
const result = await invocation.execute(abortSignal);
|
|
||||||
|
|
||||||
const expectedLlmContent = `Web search results for "grounding query":
|
|
||||||
|
|
||||||
This is a test response.
|
|
||||||
|
|
||||||
Sources:
|
|
||||||
[1] Example Site (https://example.com)
|
|
||||||
[2] Google (https://google.com)`;
|
|
||||||
|
|
||||||
expect(result.llmContent).toBe(expectedLlmContent);
|
|
||||||
expect(result.returnDisplay).toBe(
|
|
||||||
'Search results for "grounding query" returned.',
|
|
||||||
);
|
|
||||||
expect(result.sources).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
BaseDeclarativeTool,
|
|
||||||
BaseToolInvocation,
|
|
||||||
Kind,
|
|
||||||
type ToolInvocation,
|
|
||||||
type ToolResult,
|
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
type ToolInfoConfirmationDetails,
|
|
||||||
ToolConfirmationOutcome,
|
|
||||||
} from './tools.js';
|
|
||||||
|
|
||||||
import type { Config } from '../config/config.js';
|
|
||||||
import { ApprovalMode } from '../config/config.js';
|
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
|
||||||
|
|
||||||
interface TavilyResultItem {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
content?: string;
|
|
||||||
score?: number;
|
|
||||||
published_date?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TavilySearchResponse {
|
|
||||||
query: string;
|
|
||||||
answer?: string;
|
|
||||||
results: TavilyResultItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameters for the WebSearchTool.
|
|
||||||
*/
|
|
||||||
export interface WebSearchToolParams {
|
|
||||||
/**
|
|
||||||
* The search query.
|
|
||||||
*/
|
|
||||||
query: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends ToolResult to include sources for web search.
|
|
||||||
*/
|
|
||||||
export interface WebSearchToolResult extends ToolResult {
|
|
||||||
sources?: Array<{ title: string; url: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSearchToolInvocation extends BaseToolInvocation<
|
|
||||||
WebSearchToolParams,
|
|
||||||
WebSearchToolResult
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
private readonly config: Config,
|
|
||||||
params: WebSearchToolParams,
|
|
||||||
) {
|
|
||||||
super(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
override getDescription(): string {
|
|
||||||
return `Searching the web for: "${this.params.query}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
override async shouldConfirmExecute(
|
|
||||||
_abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
|
||||||
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmationDetails: ToolInfoConfirmationDetails = {
|
|
||||||
type: 'info',
|
|
||||||
title: 'Confirm Web Search',
|
|
||||||
prompt: `Search the web for: "${this.params.query}"`,
|
|
||||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
|
||||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
|
||||||
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return confirmationDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(signal: AbortSignal): Promise<WebSearchToolResult> {
|
|
||||||
const apiKey = this.config.getTavilyApiKey();
|
|
||||||
if (!apiKey) {
|
|
||||||
return {
|
|
||||||
llmContent:
|
|
||||||
'Web search is disabled because TAVILY_API_KEY is not configured. Please set it in your settings.json, .env file, or via --tavily-api-key command line argument to enable web search.',
|
|
||||||
returnDisplay:
|
|
||||||
'Web search disabled. Configure TAVILY_API_KEY to enable Tavily search.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('https://api.tavily.com/search', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
api_key: apiKey,
|
|
||||||
query: this.params.query,
|
|
||||||
search_depth: 'advanced',
|
|
||||||
max_results: 5,
|
|
||||||
include_answer: true,
|
|
||||||
}),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => '');
|
|
||||||
throw new Error(
|
|
||||||
`Tavily API error: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as TavilySearchResponse;
|
|
||||||
|
|
||||||
const sources = (data.results || []).map((r) => ({
|
|
||||||
title: r.title,
|
|
||||||
url: r.url,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sourceListFormatted = sources.map(
|
|
||||||
(s, i) => `[${i + 1}] ${s.title || 'Untitled'} (${s.url})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let content = data.answer?.trim() || '';
|
|
||||||
if (!content) {
|
|
||||||
// Fallback: build a concise summary from top results
|
|
||||||
content = sources
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((s, i) => `${i + 1}. ${s.title} - ${s.url}`)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceListFormatted.length > 0) {
|
|
||||||
content += `\n\nSources:\n${sourceListFormatted.join('\n')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content.trim()) {
|
|
||||||
return {
|
|
||||||
llmContent: `No search results or information found for query: "${this.params.query}"`,
|
|
||||||
returnDisplay: 'No information found.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
llmContent: `Web search results for "${this.params.query}":\n\n${content}`,
|
|
||||||
returnDisplay: `Search results for "${this.params.query}" returned.`,
|
|
||||||
sources,
|
|
||||||
};
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = `Error during web search for query "${this.params.query}": ${getErrorMessage(
|
|
||||||
error,
|
|
||||||
)}`;
|
|
||||||
console.error(errorMessage, error);
|
|
||||||
return {
|
|
||||||
llmContent: `Error: ${errorMessage}`,
|
|
||||||
returnDisplay: `Error performing web search.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A tool to perform web searches using Tavily Search via the Tavily API.
|
|
||||||
*/
|
|
||||||
export class WebSearchTool extends BaseDeclarativeTool<
|
|
||||||
WebSearchToolParams,
|
|
||||||
WebSearchToolResult
|
|
||||||
> {
|
|
||||||
static readonly Name: string = 'web_search';
|
|
||||||
|
|
||||||
constructor(private readonly config: Config) {
|
|
||||||
super(
|
|
||||||
WebSearchTool.Name,
|
|
||||||
'WebSearch',
|
|
||||||
'Performs a web search using the Tavily API and returns a concise answer with sources. Requires the TAVILY_API_KEY environment variable.',
|
|
||||||
Kind.Search,
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
query: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The search query to find information on the web.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['query'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the parameters for the WebSearchTool.
|
|
||||||
* @param params The parameters to validate
|
|
||||||
* @returns An error message string if validation fails, null if valid
|
|
||||||
*/
|
|
||||||
protected override validateToolParamValues(
|
|
||||||
params: WebSearchToolParams,
|
|
||||||
): string | null {
|
|
||||||
if (!params.query || params.query.trim() === '') {
|
|
||||||
return "The 'query' parameter cannot be empty.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createInvocation(
|
|
||||||
params: WebSearchToolParams,
|
|
||||||
): ToolInvocation<WebSearchToolParams, WebSearchToolResult> {
|
|
||||||
return new WebSearchToolInvocation(this.config, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,34 +108,36 @@ class WebSearchToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the appropriate provider based on configuration and parameters.
|
* Select the appropriate provider based on configuration and parameters.
|
||||||
|
* Throws error if provider not found.
|
||||||
*/
|
*/
|
||||||
private selectProvider(
|
private selectProvider(
|
||||||
providers: Map<string, WebSearchProvider>,
|
providers: Map<string, WebSearchProvider>,
|
||||||
requestedProvider?: string,
|
requestedProvider?: string,
|
||||||
defaultProvider?: string,
|
defaultProvider?: string,
|
||||||
): { provider: WebSearchProvider | null; error?: string } {
|
): WebSearchProvider {
|
||||||
// Use requested provider if specified
|
// Use requested provider if specified
|
||||||
if (requestedProvider) {
|
if (requestedProvider) {
|
||||||
const provider = providers.get(requestedProvider);
|
const provider = providers.get(requestedProvider);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
const availableProviders = Array.from(providers.keys()).join(', ');
|
const available = Array.from(providers.keys()).join(', ');
|
||||||
return {
|
throw new Error(
|
||||||
provider: null,
|
`The specified provider "${requestedProvider}" is not available. Available: ${available}`,
|
||||||
error: `The specified provider "${requestedProvider}" is not available or not configured. Available providers: ${availableProviders}`,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { provider };
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use default provider if specified and available
|
// Use default provider if specified and available
|
||||||
if (defaultProvider && providers.has(defaultProvider)) {
|
if (defaultProvider && providers.has(defaultProvider)) {
|
||||||
const provider = providers.get(defaultProvider)!;
|
return providers.get(defaultProvider)!;
|
||||||
return { provider };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to first available provider
|
// Fallback to first available provider
|
||||||
const firstProvider = providers.values().next().value;
|
const firstProvider = providers.values().next().value;
|
||||||
return { provider: firstProvider || null };
|
if (!firstProvider) {
|
||||||
|
throw new Error('No web search providers are available.');
|
||||||
|
}
|
||||||
|
return firstProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,76 +168,55 @@ class WebSearchToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async execute(signal: AbortSignal): Promise<WebSearchToolResult> {
|
async execute(signal: AbortSignal): Promise<WebSearchToolResult> {
|
||||||
|
// Guard: Check configuration exists
|
||||||
const webSearchConfig = this.config.getWebSearchConfig();
|
const webSearchConfig = this.config.getWebSearchConfig();
|
||||||
if (!webSearchConfig) {
|
if (!webSearchConfig) {
|
||||||
|
const message =
|
||||||
|
'Web search is disabled. Please configure web search providers in settings.json.';
|
||||||
return {
|
return {
|
||||||
llmContent:
|
llmContent: message,
|
||||||
'Web search is disabled because no web search configuration is available. Please configure web search providers in your settings.json.',
|
|
||||||
returnDisplay:
|
returnDisplay:
|
||||||
'Web search disabled. Configure web search providers to enable search.',
|
'Web search disabled. Configure providers to enable search.',
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const providers = this.createProviders(webSearchConfig.provider);
|
|
||||||
|
|
||||||
const { provider: selectedProvider, error } = this.selectProvider(
|
|
||||||
providers,
|
|
||||||
this.params.provider,
|
|
||||||
webSearchConfig.default,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
llmContent: error,
|
|
||||||
returnDisplay: `Provider "${this.params.provider}" not available.`,
|
|
||||||
error: {
|
error: {
|
||||||
message: error,
|
message,
|
||||||
type: ToolErrorType.INVALID_TOOL_PARAMS,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedProvider) {
|
|
||||||
const errorMsg =
|
|
||||||
'Web search is disabled because no web search providers are available. Please check your configuration.';
|
|
||||||
return {
|
|
||||||
llmContent: errorMsg,
|
|
||||||
returnDisplay: 'Web search disabled. No available providers.',
|
|
||||||
error: {
|
|
||||||
message: errorMsg,
|
|
||||||
type: ToolErrorType.EXECUTION_FAILED,
|
type: ToolErrorType.EXECUTION_FAILED,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchResult = await selectedProvider.search(
|
// Create and select provider
|
||||||
this.params.query,
|
const providers = this.createProviders(webSearchConfig.provider);
|
||||||
signal,
|
const provider = this.selectProvider(
|
||||||
|
providers,
|
||||||
|
this.params.provider,
|
||||||
|
webSearchConfig.default,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
const searchResult = await provider.search(this.params.query, signal);
|
||||||
const { content, sources } = this.formatSearchResults(searchResult);
|
const { content, sources } = this.formatSearchResults(searchResult);
|
||||||
|
|
||||||
|
// Guard: Check if we got results
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return {
|
return {
|
||||||
llmContent: `No search results or information found for query: "${this.params.query}" (searched via ${selectedProvider.name})`,
|
llmContent: `No search results found for query: "${this.params.query}" (via ${provider.name})`,
|
||||||
returnDisplay: `No information found for "${this.params.query}".`,
|
returnDisplay: `No information found for "${this.params.query}".`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Success result
|
||||||
return {
|
return {
|
||||||
llmContent: `Web search results for "${this.params.query}" (via ${selectedProvider.name}):\n\n${content}`,
|
llmContent: `Web search results for "${this.params.query}" (via ${provider.name}):\n\n${content}`,
|
||||||
returnDisplay: `Search results for "${this.params.query}".`,
|
returnDisplay: `Search results for "${this.params.query}".`,
|
||||||
sources,
|
sources,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = `Error during web search for query "${this.params.query}": ${getErrorMessage(
|
const errorMessage = `Error during web search: ${getErrorMessage(error)}`;
|
||||||
error,
|
|
||||||
)}`;
|
|
||||||
console.error(errorMessage, error);
|
console.error(errorMessage, error);
|
||||||
return {
|
return {
|
||||||
llmContent: `Error: ${errorMessage}`,
|
llmContent: errorMessage,
|
||||||
returnDisplay: `Error performing web search.`,
|
returnDisplay: 'Error performing web search.',
|
||||||
error: {
|
error: {
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
type: ToolErrorType.EXECUTION_FAILED,
|
type: ToolErrorType.EXECUTION_FAILED,
|
||||||
|
|||||||
Reference in New Issue
Block a user