diff --git a/docs/tools/web-search.md b/docs/tools/web-search.md index 7f78cf70..60617d71 100644 --- a/docs/tools/web-search.md +++ b/docs/tools/web-search.md @@ -1,43 +1,165 @@ # 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 -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 -`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` -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 +Add to your `settings.json`: -If the key is not configured, the tool will be disabled and skipped. - -Usage: - -``` -web_search(query="Your query goes here.") +```json +{ + "webSearch": { + "provider": [ + { "type": "dashscope" }, + { "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 -``` -web_search(query="latest advancements in AI-powered code generation") +### Method 2: Environment Variables + +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. -- **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. +Pass API keys when running Qwen Code: + +```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) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 9655a1ec..61a856d6 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -43,6 +43,7 @@ import { mcpCommand } from '../commands/mcp.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; +import { buildWebSearchConfig } from './webSearch.js'; // Simple console logger for now - replace with actual logger if available const logger = { @@ -117,6 +118,9 @@ export interface CliArgs { proxy: string | undefined; includeDirectories: string[] | undefined; tavilyApiKey: string | undefined; + googleApiKey: string | undefined; + googleSearchEngineId: string | undefined; + webSearchDefault: string | undefined; screenReader: boolean | undefined; vlmSwitchMode: string | undefined; useSmartEdit: boolean | undefined; @@ -325,7 +329,20 @@ export async function parseArguments(settings: Settings): Promise { }) .option('tavily-api-key', { 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', { type: 'boolean', @@ -747,10 +764,7 @@ export async function loadCliConfig( : argv.openaiLogging) ?? false, }, cliVersion: await getCliVersion(), - tavilyApiKey: - argv.tavilyApiKey || - settings.advanced?.tavilyApiKey || - process.env['TAVILY_API_KEY'], + webSearch: buildWebSearchConfig(argv, settings), summarizeToolOutput: settings.model?.summarizeToolOutput, ideMode, chatCompression: settings.model?.chatCompression, @@ -771,7 +785,6 @@ export async function loadCliConfig( output: { format: (argv.outputFormat ?? settings.output?.format) as OutputFormat, }, - webSearch: settings.webSearch, }); } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 4265cc5c..509f7666 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1044,12 +1044,12 @@ const SETTINGS_SCHEMA = { }, tavilyApiKey: { type: 'string', - label: 'Tavily API Key', + label: 'Tavily API Key (Deprecated)', category: 'Advanced', requiresRestart: false, default: undefined as string | undefined, 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, }, }, diff --git a/packages/cli/src/config/webSearch.ts b/packages/cli/src/config/webSearch.ts new file mode 100644 index 00000000..a558de17 --- /dev/null +++ b/packages/cli/src/config/webSearch.ts @@ -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, + }; +} diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 231c34a7..76d2c772 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -330,6 +330,9 @@ describe('gemini.tsx main function kitty protocol', () => { proxy: undefined, includeDirectories: undefined, tavilyApiKey: undefined, + googleApiKey: undefined, + googleSearchEngineId: undefined, + webSearchDefault: undefined, screenReader: undefined, vlmSwitchMode: undefined, useSmartEdit: undefined, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 74dbef22..c3b4dbc1 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -260,7 +260,6 @@ export interface ConfigParameters { cliVersion?: string; loadMemoryFromIncludeDirectories?: boolean; // Web search providers - tavilyApiKey?: string; webSearch?: { provider: Array<{ type: 'tavily' | 'google' | 'dashscope'; @@ -356,7 +355,6 @@ export class Config { private readonly cliVersion?: string; private readonly experimentalZedIntegration: boolean = false; private readonly loadMemoryFromIncludeDirectories: boolean = false; - private readonly tavilyApiKey?: string; private readonly webSearch?: { provider: Array<{ type: 'tavily' | 'google' | 'dashscope'; @@ -469,7 +467,6 @@ export class Config { this.skipLoopDetection = params.skipLoopDetection ?? false; // Web search - this.tavilyApiKey = params.tavilyApiKey; this.webSearch = params.webSearch; this.useRipgrep = params.useRipgrep ?? true; this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false; @@ -904,10 +901,6 @@ export class Config { } // Web search provider configuration - getTavilyApiKey(): string | undefined { - return this.tavilyApiKey; - } - getWebSearchConfig() { return this.webSearch; } diff --git a/packages/core/src/tools/web-search.test.ts b/packages/core/src/tools/web-search.test.ts deleted file mode 100644 index 5b0a8f26..00000000 --- a/packages/core/src/tools/web-search.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts deleted file mode 100644 index e99997a8..00000000 --- a/packages/core/src/tools/web-search.ts +++ /dev/null @@ -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 { - 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 { - 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 { - return new WebSearchToolInvocation(this.config, params); - } -} diff --git a/packages/core/src/tools/web-search/index.ts b/packages/core/src/tools/web-search/index.ts index 06e3cc5f..52b62266 100644 --- a/packages/core/src/tools/web-search/index.ts +++ b/packages/core/src/tools/web-search/index.ts @@ -108,34 +108,36 @@ class WebSearchToolInvocation extends BaseToolInvocation< /** * Select the appropriate provider based on configuration and parameters. + * Throws error if provider not found. */ private selectProvider( providers: Map, requestedProvider?: string, defaultProvider?: string, - ): { provider: WebSearchProvider | null; error?: string } { + ): WebSearchProvider { // Use requested provider if specified if (requestedProvider) { const provider = providers.get(requestedProvider); if (!provider) { - const availableProviders = Array.from(providers.keys()).join(', '); - return { - provider: null, - error: `The specified provider "${requestedProvider}" is not available or not configured. Available providers: ${availableProviders}`, - }; + const available = Array.from(providers.keys()).join(', '); + throw new Error( + `The specified provider "${requestedProvider}" is not available. Available: ${available}`, + ); } - return { provider }; + return provider; } // Use default provider if specified and available if (defaultProvider && providers.has(defaultProvider)) { - const provider = providers.get(defaultProvider)!; - return { provider }; + return providers.get(defaultProvider)!; } // Fallback to first available provider 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 { + // Guard: Check configuration exists const webSearchConfig = this.config.getWebSearchConfig(); if (!webSearchConfig) { + const message = + 'Web search is disabled. Please configure web search providers in settings.json.'; return { - llmContent: - 'Web search is disabled because no web search configuration is available. Please configure web search providers in your settings.json.', + llmContent: message, returnDisplay: - 'Web search disabled. Configure web search 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.`, + 'Web search disabled. Configure providers to enable search.', error: { - message: error, - 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, + message, type: ToolErrorType.EXECUTION_FAILED, }, }; } try { - const searchResult = await selectedProvider.search( - this.params.query, - signal, + // Create and select provider + const providers = this.createProviders(webSearchConfig.provider); + 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); + // Guard: Check if we got results if (!content.trim()) { 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}".`, }; } + // Success result 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}".`, sources, }; } catch (error: unknown) { - const errorMessage = `Error during web search for query "${this.params.query}": ${getErrorMessage( - error, - )}`; + const errorMessage = `Error during web search: ${getErrorMessage(error)}`; console.error(errorMessage, error); return { - llmContent: `Error: ${errorMessage}`, - returnDisplay: `Error performing web search.`, + llmContent: errorMessage, + returnDisplay: 'Error performing web search.', error: { message: errorMessage, type: ToolErrorType.EXECUTION_FAILED,