diff --git a/.qwen/agents/test-engineer.md b/.qwen/agents/test-engineer.md index 61be283d5..395e281af 100644 --- a/.qwen/agents/test-engineer.md +++ b/.qwen/agents/test-engineer.md @@ -18,7 +18,6 @@ tools: - run_shell_command - skill - web_fetch - - web_search --- # Test Engineer — Bug Reproduction & Verification diff --git a/docs/developers/tools/introduction.md b/docs/developers/tools/introduction.md index 9c7325552..1dafb14c8 100644 --- a/docs/developers/tools/introduction.md +++ b/docs/developers/tools/introduction.md @@ -46,7 +46,6 @@ Qwen Code's built-in tools can be broadly categorized as follows: - **[File System Tools](./file-system.md):** For interacting with files and directories (reading, writing, listing, searching, etc.). - **[Shell Tool](./shell.md) (`run_shell_command`):** For executing shell commands. - **[Web Fetch Tool](./web-fetch.md) (`web_fetch`):** For retrieving content from URLs. -- **[Web Search Tool](./web-search.md) (`web_search`):** For searching the web. - **[Multi-File Read Tool](./multi-file.md) (`read_many_files`):** A specialized tool for reading content from multiple files or directories, often used by the `@` command. - **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling information across sessions. - **[Todo Write Tool](./todo-write.md) (`todo_write`):** For creating and managing structured task lists during coding sessions. @@ -58,5 +57,6 @@ Additionally, these tools incorporate: - **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the model and your local environment or other services like APIs. - **[MCP Quick Start Guide](../mcp-quick-start.md)**: Get started with MCP in 5 minutes with practical examples - **[MCP Example Configurations](../mcp-example-configs.md)**: Ready-to-use configurations for common scenarios + - **[Web Search via MCP](./web-search.md)**: Connect to web search services (Bailian, Tavily, GLM) through MCP - **[MCP Testing & Validation](../mcp-testing-validation.md)**: Test and validate your MCP server setups - **[Sandboxing](../sandbox.md)**: Sandboxing isolates the model and its changes from your environment to reduce potential risk. diff --git a/docs/developers/tools/web-search.md b/docs/developers/tools/web-search.md index dd1fd7ec6..c55790891 100644 --- a/docs/developers/tools/web-search.md +++ b/docs/developers/tools/web-search.md @@ -1,185 +1,215 @@ -# Web Search Tool (`web_search`) +# Web Search -This document describes the `web_search` tool for performing web searches using multiple providers. +Qwen Code supports web search capabilities through **MCP (Model Context Protocol)** integrations. Rather than a built-in search tool, web search is provided by connecting to external MCP servers, giving you full flexibility to choose the search service that best fits your needs. -## Description +## ⚠️ Breaking Change: Built-in `web_search` Tool Removed -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. +> **Affected versions:** `V0.0.7+` through the last release with built-in web search support. -### Supported Providers +The built-in `web_search` tool and all its associated configuration have been **removed**. If you were using any of the following, you should migrate to the MCP-based approach described in this document: -1. **DashScope** (Official) - Available when explicitly configured in settings (Qwen OAuth free tier auto-injection discontinued 2026-04-15) -2. **Tavily** - High-quality search API with built-in answer generation -3. **Google Custom Search** - Google's Custom Search JSON API +| Removed | What to do | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `webSearch` block in `settings.json` | Configure an MCP server in `mcpServers` instead (see below) | +| `advanced.tavilyApiKey` in `settings.json` | Use the [Tavily MCP server](#tavily-websearch) | +| `TAVILY_API_KEY` environment variable | Use the [Tavily MCP server](#tavily-websearch) | +| `DASHSCOPE_API_KEY` for web search | Use the [Alibaba Cloud Bailian WebSearch MCP](#alibaba-cloud-bailian-websearch-recommended) | +| `GLM_API_KEY` for web search | Use the [GLM WebSearch Prime MCP](#glm-websearch-prime-zhipuai) | +| `--tavily-api-key` / `--glm-api-key` / `--dashscope-api-key` CLI flags | Configure via `mcpServers` in `settings.json` | -### Arguments +### Migration Examples -`web_search` takes two arguments: - -- `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 - -## Configuration - -### Method 1: Settings File (Recommended) - -Add to your `settings.json`: +**Before (Tavily via built-in tool):** ```json { "webSearch": { - "provider": [ - { "type": "dashscope" }, - { "type": "tavily", "apiKey": "tvly-xxxxx" }, - { - "type": "google", - "apiKey": "your-google-api-key", - "searchEngineId": "your-search-engine-id" - } - ], + "provider": [{ "type": "tavily", "apiKey": "tvly-xxx" }], + "default": "tavily" + } +} +``` + +**After (Tavily via MCP):** + +```json +{ + "mcpServers": { + "tavily": { + "httpUrl": "https://mcp.tavily.com/mcp/?tavilyApiKey=tvly-xxx" + } + } +} +``` + +--- + +**Before (DashScope via built-in tool):** + +```json +{ + "webSearch": { + "provider": [{ "type": "dashscope", "apiKey": "sk-xxx" }], "default": "dashscope" } } ``` -**Notes:** - -- DashScope doesn't require an API key (official, free service) -- **Qwen OAuth users:** DashScope is automatically added to your provider list, even if not explicitly configured -- Configure additional providers (Tavily, Google) if you want to use them alongside DashScope -- Set `default` to specify which provider to use by default (if not set, priority order: Tavily > Google > DashScope) - -### 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" -``` - -### Method 3: Command Line Arguments - -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: +**After (Alibaba Cloud Bailian WebSearch via MCP):** ```json { - "advanced": { - "tavilyApiKey": "tvly-xxxxx" // ⚠️ Deprecated + "mcpServers": { + "WebSearch": { + "httpUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp", + "headers": { + "Authorization": "Bearer sk-xxx" + } + } } } ``` -**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. +--- -## Disabling Web Search +## Supported MCP Web Search Services -If you want to disable the web search functionality, you can exclude the `web_search` tool in your `settings.json`: +### Alibaba Cloud Bailian WebSearch (Recommended) + +The official web search MCP service provided by Alibaba Cloud Bailian platform, powered by DashScope. + +- **MCP Marketplace:** https://bailian.console.aliyun.com/cn-beijing?tab=mcp#/mcp-market/detail/WebSearch +- **Cost:** Paid (billed via Alibaba Cloud DashScope) +- **Get API Key:** https://help.aliyun.com/zh/model-studio/get-api-key +- **Best for:** Chinese-language queries, access to Chinese web content, integration with the Alibaba Cloud ecosystem + +#### Setup + +**Method 1: CLI command** + +```bash +qwen mcp add WebSearch \ + -t http \ + "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp" \ + -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" +``` + +**Method 2: `settings.json`** ```json { - "tools": { - "exclude": ["web_search"] + "mcpServers": { + "WebSearch": { + "httpUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp", + "headers": { + "Authorization": "Bearer ${DASHSCOPE_API_KEY}" + } + } } } ``` -**Note:** This setting requires a restart of Qwen Code to take effect. Once disabled, the `web_search` tool will not be available to the model, even if web search providers are configured. +Replace `${DASHSCOPE_API_KEY}` with your actual API key, or set it as an environment variable so Qwen Code picks it up automatically. -## Usage Examples +--- -### Basic search (using default provider) +### Tavily WebSearch -``` -web_search(query="latest advancements in AI") +A production-ready MCP server providing real-time web search, extract, map, and crawl capabilities. + +- **Repository:** https://github.com/tavily-ai/tavily-mcp +- **Cost:** Paid (free tier available) +- **Get API Key:** https://app.tavily.com/home +- **Best for:** General-purpose web search with high-quality AI-generated answers + +#### Available Tools + +- `tavily_search` — Real-time web search +- `tavily_extract` — Intelligent data extraction from web pages +- `tavily_map` — Create a structured map of a website +- `tavily_crawl` — Systematically explore websites + +#### Setup + +**Method 1: CLI command (Remote MCP)** + +```bash +qwen mcp add tavily \ + -t http \ + "https://mcp.tavily.com/mcp/?tavilyApiKey=${TAVILY_API_KEY}" ``` -### Search with specific provider +**Method 2: `settings.json` (Remote MCP)** -``` -web_search(query="latest advancements in AI", provider="tavily") +```json +{ + "mcpServers": { + "tavily": { + "httpUrl": "https://mcp.tavily.com/mcp/?tavilyApiKey=${TAVILY_API_KEY}" + } + } +} ``` -### Real-world examples +Replace `${TAVILY_API_KEY}` with your actual API key, or set it as an environment variable. -``` -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") +**Method 3: `settings.json` (Local NPX)** + +```json +{ + "mcpServers": { + "tavily-mcp": { + "command": "npx", + "args": ["-y", "tavily-mcp@latest"], + "env": { + "TAVILY_API_KEY": "your-api-key-here" + } + } + } +} ``` -## Provider Details +--- -### DashScope (Official) +### GLM WebSearch Prime (ZhipuAI) -- **Cost:** Free (requires Qwen OAuth credentials) -- **Authentication:** Requires Qwen OAuth credentials -- **Configuration:** Must be explicitly configured in `settings.json` web search providers (auto-injection for Qwen OAuth users was removed when the free tier was discontinued on 2026-04-15) -- **Quota:** 200 requests/minute, 100 requests/day -- **Best for:** General queries when you have Qwen OAuth credentials +The official web search Remote MCP service provided by ZhipuAI (智谱AI), designed for GLM Coding Plan users. Provides real-time web search including news, stock prices, weather, and more. -### Tavily +- **Documentation:** https://docs.bigmodel.cn/cn/coding-plan/mcp/search-mcp-server +- **Cost:** Included in GLM Coding Plan subscription (Lite: 100 calls/month, Pro: 1,000/month, Max: 4,000/month) +- **Get API Key:** https://open.bigmodel.cn/apikey/platform +- **Best for:** Chinese-language queries, real-time information retrieval -- **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 +#### Available Tools -### Google Custom Search +- `webSearchPrime` — Web search returning page title, URL, summary, site name, and favicon -- **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 +#### Setup -## Important Notes +**Method 1: CLI command** -- **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:** Automatically available for Qwen OAuth users, no configuration needed -- **Default provider selection:** The system automatically selects a default provider based on availability: - 1. Your explicit `default` configuration (highest priority) - 2. CLI argument `--web-search-default` - 3. First available provider by priority: Tavily > Google > DashScope +```bash +qwen mcp add web-search-prime \ + -t http \ + "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp" \ + -H "Authorization: Bearer ${GLM_API_KEY}" +``` -## Troubleshooting +**Method 2: `settings.json`** -**Tool not available?** +```json +{ + "mcpServers": { + "web-search-prime": { + "httpUrl": "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp", + "headers": { + "Authorization": "Bearer ${GLM_API_KEY}" + } + } + } +} +``` -- **For Qwen OAuth users:** The tool is automatically registered with DashScope provider, no configuration needed -- **For other authentication types:** Ensure at least one provider (Tavily or Google) is configured -- For Tavily/Google: Verify your API keys are correct +Replace `${GLM_API_KEY}` with your actual ZhipuAI API key, or set it as an environment variable. -**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/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 6dc6d1d02..a848388fa 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -430,11 +430,6 @@ LSP server configuration is done through `.lsp.json` files in your project root | `advanced.dnsResolutionOrder` | string | The DNS resolution order. | `undefined` | | `advanced.excludedEnvVars` | array of strings | Environment variables to exclude from project context. Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with the CLI behavior. Variables from `.qwen/.env` files are never excluded. | `["DEBUG","DEBUG_MODE"]` | | `advanced.bugCommand` | object | Configuration for the bug report command. Overrides the default URL for the `/bug` command. Properties: `urlTemplate` (string): A URL that can contain `{title}` and `{info}` placeholders. Example: `"bugCommand": { "urlTemplate": "https://bug.example.com/new?title={title}&info={info}" }` | `undefined` | -| `advanced.tavilyApiKey` | string | API key for Tavily web search service. Used to enable the `web_search` tool functionality. | `undefined` | - -> [!note] -> -> **Note about advanced.tavilyApiKey:** This is a legacy configuration format. For Qwen OAuth users, DashScope provider is automatically available without any configuration. For other authentication types, configure Tavily or Google providers using the new `webSearch` configuration format. #### mcpServers @@ -571,7 +566,6 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe | `CLI_TITLE` | Set to a string to customize the title of the CLI. | | | `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. | | `QWEN_CODE_MAX_OUTPUT_TOKENS` | Overrides the default maximum output tokens per response. When not set, Qwen Code uses an adaptive strategy: starts with 8K tokens and automatically retries with 64K if the response is truncated. Set this to a specific value (e.g., `16000`) to use a fixed limit instead. | Takes precedence over the capped default (8K) but is overridden by `samplingParams.max_tokens` in settings. Disables automatic escalation when set. Example: `export QWEN_CODE_MAX_OUTPUT_TOKENS=16000` | -| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` | | `QWEN_CODE_UNATTENDED_RETRY` | Set to `true` or `1` to enable persistent retry mode. When enabled, transient API capacity errors (HTTP 429 Rate Limit and 529 Overloaded) are retried indefinitely with exponential backoff (capped at 5 minutes per retry) and heartbeat keepalives every 30 seconds on stderr. | Designed for CI/CD pipelines and background automation where long-running tasks should survive temporary API outages. Must be set explicitly — `CI=true` alone does **not** activate this mode. See [Headless Mode](../features/headless#persistent-retry-mode) for details. Example: `export QWEN_CODE_UNATTENDED_RETRY=1` | | `QWEN_CODE_PROFILE_STARTUP` | Set to `1` to enable startup performance profiling. Writes a JSON timing report to `~/.qwen/startup-perf/` with per-phase durations. | Only active inside the sandbox child process. Zero overhead when not set. Example: `export QWEN_CODE_PROFILE_STARTUP=1` | @@ -620,7 +614,6 @@ For sandbox image selection, precedence is: | `--version` | | Displays the version of the CLI. | | | | `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. | | `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` | -| `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` | ## Context Files (Hierarchical Instructional Context) diff --git a/docs/users/features/sub-agents.md b/docs/users/features/sub-agents.md index 957262a6a..d4473572e 100644 --- a/docs/users/features/sub-agents.md +++ b/docs/users/features/sub-agents.md @@ -333,7 +333,6 @@ tools: - read_file - write_file - read_many_files - - web_search --- You are a technical documentation specialist. diff --git a/integration-tests/cli/web_search.test.ts b/integration-tests/cli/web_search.test.ts deleted file mode 100644 index 5ab0b4364..000000000 --- a/integration-tests/cli/web_search.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect } from 'vitest'; -import { - TestRig, - printDebugInfo, - validateModelOutput, -} from '../test-helper.js'; - -describe('web_search', () => { - it('should be able to search the web', async () => { - // Check if any web search provider is available - const hasTavilyKey = !!process.env['TAVILY_API_KEY']; - const hasGoogleKey = - !!process.env['GOOGLE_API_KEY'] && - !!process.env['GOOGLE_SEARCH_ENGINE_ID']; - - // Skip if no provider is configured - // Note: DashScope provider is automatically available for Qwen OAuth users, - // but we can't easily detect that in tests without actual OAuth credentials - if (!hasTavilyKey && !hasGoogleKey) { - console.warn( - 'Skipping web search test: No web search provider configured. ' + - 'Set TAVILY_API_KEY or GOOGLE_API_KEY+GOOGLE_SEARCH_ENGINE_ID environment variables.', - ); - return; - } - - const rig = new TestRig(); - // Configure web search in settings if provider keys are available - const webSearchSettings: Record = {}; - const providers: Array<{ - type: string; - apiKey?: string; - searchEngineId?: string; - }> = []; - - if (hasTavilyKey) { - providers.push({ type: 'tavily', apiKey: process.env['TAVILY_API_KEY'] }); - } - if (hasGoogleKey) { - providers.push({ - type: 'google', - apiKey: process.env['GOOGLE_API_KEY'], - searchEngineId: process.env['GOOGLE_SEARCH_ENGINE_ID'], - }); - } - - if (providers.length > 0) { - webSearchSettings.webSearch = { - provider: providers, - default: providers[0]?.type, - }; - } - - await rig.setup('should be able to search the web', { - settings: webSearchSettings, - }); - - let result; - try { - result = await rig.run(`what is the weather in London`); - } catch (error) { - // Network errors can occur in CI environments - if ( - error instanceof Error && - (error.message.includes('network') || error.message.includes('timeout')) - ) { - console.warn( - 'Skipping test due to network error:', - (error as Error).message, - ); - return; // Skip the test - } - throw error; // Re-throw if not a network error - } - - const foundToolCall = await rig.waitForToolCall('web_search'); - - // Add debugging information - if (!foundToolCall) { - const allTools = printDebugInfo(rig, result); - - // Check if the tool call failed due to network issues - const failedSearchCalls = allTools.filter( - (t) => t.toolRequest.name === 'web_search' && !t.toolRequest.success, - ); - if (failedSearchCalls.length > 0) { - console.warn( - 'web_search tool was called but failed, possibly due to network issues', - ); - console.warn( - 'Failed calls:', - failedSearchCalls.map((t) => t.toolRequest.args), - ); - return; // Skip the test if network issues - } - } - - expect(foundToolCall, 'Expected to find a call to web_search').toBeTruthy(); - - // Validate model output - will throw if no output, warn if missing expected content - const hasExpectedContent = validateModelOutput( - result, - ['weather', 'london'], - 'Web search test', - ); - - // If content was missing, log the search queries used - if (!hasExpectedContent) { - const searchCalls = rig - .readToolLogs() - .filter((t) => t.toolRequest.name === 'web_search'); - if (searchCalls.length > 0) { - console.warn( - 'Search queries used:', - searchCalls.map((t) => t.toolRequest.args), - ); - } - } - }); -}); diff --git a/integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js b/integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js index abb893b1c..5bcfd6d71 100644 --- a/integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js +++ b/integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js @@ -530,7 +530,6 @@ const TOOL_DISPLAY_NAME_BY_NAME = { skill: 'Skill', exit_plan_mode: 'ExitPlanMode', web_fetch: 'WebFetch', - web_search: 'WebSearch', list_directory: 'ListFiles', }; @@ -546,7 +545,6 @@ const TOOL_KIND_BY_NAME = { rename: 'move', grep_search: 'search', glob: 'search', - web_search: 'search', list_directory: 'search', run_shell_command: 'execute', bash: 'execute', diff --git a/integration-tests/sdk-typescript/permission-control.test.ts b/integration-tests/sdk-typescript/permission-control.test.ts index 5ea241db7..2ee6e8a8d 100644 --- a/integration-tests/sdk-typescript/permission-control.test.ts +++ b/integration-tests/sdk-typescript/permission-control.test.ts @@ -905,7 +905,6 @@ describe('Permission Control (E2E)', () => { 'grep_search', 'glob', 'list_directory', - 'web_search', 'web_fetch', ]; diff --git a/packages/cli/src/commands/auth/handler.ts b/packages/cli/src/commands/auth/handler.ts index dd3421b6c..25a7d44fa 100644 --- a/packages/cli/src/commands/auth/handler.ts +++ b/packages/cli/src/commands/auth/handler.ts @@ -91,10 +91,6 @@ export async function handleQwenAuth( openaiLoggingDir: undefined, proxy: undefined, includeDirectories: undefined, - tavilyApiKey: undefined, - googleApiKey: undefined, - googleSearchEngineId: undefined, - webSearchDefault: undefined, screenReader: undefined, inputFormat: undefined, outputFormat: undefined, diff --git a/packages/cli/src/commands/extensions/examples/agent/agents/diary.md b/packages/cli/src/commands/extensions/examples/agent/agents/diary.md index 8c0c76a91..45eea1424 100644 --- a/packages/cli/src/commands/extensions/examples/agent/agents/diary.md +++ b/packages/cli/src/commands/extensions/examples/agent/agents/diary.md @@ -11,7 +11,6 @@ tools: - NotebookRead - WebFetch - TodoWrite - - WebSearch modelConfig: model: qwen3-coder-plus --- diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 01ad506b6..4c18efcc3 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1734,7 +1734,6 @@ describe('loadCliConfig with includeDirectories', () => { expect(config.getToolDiscoveryCommand()).toBeUndefined(); expect(config.getToolCallCommand()).toBeUndefined(); expect(config.getMcpServers()).toEqual({}); - expect(config.getWebSearchConfig()).toBeUndefined(); expect(config.isLspEnabled()).toBe(false); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index d85d11941..304f878ac 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -67,7 +67,6 @@ export function isValidSessionId(value: string): boolean { } import { isWorkspaceTrusted } from './trustedFolders.js'; -import { buildWebSearchConfig } from './webSearch.js'; import { writeStderrLine } from '../utils/stdioHelpers.js'; const debugLogger = createDebugLogger('CONFIG'); @@ -138,10 +137,6 @@ export interface CliArgs { openaiLoggingDir: string | undefined; proxy: string | undefined; includeDirectories: string[] | undefined; - tavilyApiKey: string | undefined; - googleApiKey: string | undefined; - googleSearchEngineId: string | undefined; - webSearchDefault: string | undefined; screenReader: boolean | undefined; inputFormat?: string | undefined; outputFormat: string | undefined; @@ -431,23 +426,6 @@ export async function parseArguments(): Promise { type: 'string', description: 'OpenAI base URL (for custom endpoints)', }) - .option('tavily-api-key', { - type: 'string', - 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', description: 'Enable screen reader mode for accessibility.', @@ -1206,9 +1184,6 @@ export async function loadCliConfig( ? [] : (settings.security?.allowedHttpHookUrls ?? []), cliVersion: await getCliVersion(), - webSearch: bareMode - ? undefined - : buildWebSearchConfig(argv, settings, selectedAuthType), ideMode, chatCompression: settings.model?.chatCompression, folderTrust, diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 252db02c0..5d4363b13 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -28,7 +28,6 @@ describe('SettingsSchema', () => { 'mcp', 'security', 'advanced', - 'webSearch', ]; expectedSettings.forEach((setting) => { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 8aa7517a6..4ebb587ea 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1590,37 +1590,9 @@ const SETTINGS_SCHEMA = { 'Config files remain at ~/.qwen. Env var QWEN_RUNTIME_DIR takes priority.', showInDialog: false, }, - tavilyApiKey: { - type: 'string', - label: 'Tavily API Key (Deprecated)', - category: 'Advanced', - requiresRestart: false, - default: undefined as string | undefined, - description: - '⚠️ DEPRECATED: Please use webSearch.provider configuration instead. Legacy API key for the Tavily API.', - showInDialog: false, - }, }, }, - webSearch: { - type: 'object', - label: 'Web Search', - category: 'Advanced', - requiresRestart: true, - default: undefined as - | { - provider: Array<{ - type: 'tavily' | 'google' | 'dashscope'; - apiKey?: string; - searchEngineId?: string; - }>; - default: string; - } - | undefined, - description: 'Configuration for web search providers.', - showInDialog: false, - }, agents: { type: 'object', label: 'Agents', diff --git a/packages/cli/src/config/webSearch.ts b/packages/cli/src/config/webSearch.ts deleted file mode 100644 index 4dc8adbbe..000000000 --- a/packages/cli/src/config/webSearch.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @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 - * @param authType - Authentication type (e.g., 'qwen-oauth') - * @returns WebSearch configuration or undefined if no providers available - */ -export function buildWebSearchConfig( - argv: WebSearchCliArgs, - settings: Settings, - _authType?: string, -): WebSearchConfig | undefined { - // Step 1: Collect providers from settings or command line/env - let providers: WebSearchProviderConfig[] = []; - let userDefault: string | undefined; - - if (settings.webSearch) { - // Use providers from settings.json - providers = [...settings.webSearch.provider]; - userDefault = settings.webSearch.default; - } else { - // Build providers from command line args and environment variables - const tavilyKey = - argv.tavilyApiKey || - settings.advanced?.tavilyApiKey || - process.env['TAVILY_API_KEY']; - if (tavilyKey) { - providers.push({ - type: 'tavily', - apiKey: tavilyKey, - } as WebSearchProviderConfig); - } - - 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); - } - } - - // Step 2: DashScope auto-injection for qwen-oauth was removed when the - // free tier was discontinued on 2026-04-15. Users who explicitly configure - // a dashscope provider in settings.json still get it (handled in Step 1). - - // Step 3: If no providers available, return undefined - if (providers.length === 0) { - return undefined; - } - - // Step 4: Determine default provider - // Priority: user explicit config > CLI arg > first available provider (tavily > google > dashscope) - const providerPriority: Array<'tavily' | 'google' | 'dashscope'> = [ - 'tavily', - 'google', - 'dashscope', - ]; - - // Determine default provider based on availability - let defaultProvider = userDefault || argv.webSearchDefault; - if (!defaultProvider) { - // Find first available provider by priority order - for (const providerType of providerPriority) { - if (providers.some((p) => p.type === providerType)) { - defaultProvider = providerType; - break; - } - } - // Fallback to first available provider if none found in priority list - if (!defaultProvider) { - defaultProvider = providers[0]?.type || 'dashscope'; - } - } - - return { - provider: providers, - default: defaultProvider, - }; -} diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 1b7367984..197b8dcb0 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -595,10 +595,6 @@ describe('gemini.tsx main function kitty protocol', () => { openaiLoggingDir: undefined, proxy: undefined, includeDirectories: undefined, - tavilyApiKey: undefined, - googleApiKey: undefined, - googleSearchEngineId: undefined, - webSearchDefault: undefined, screenReader: undefined, inputFormat: undefined, outputFormat: undefined, diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 930ea41a0..cc7fe7fda 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -583,10 +583,6 @@ export async function start_sandbox( if (process.env['OPENAI_API_KEY']) { args.push('--env', `OPENAI_API_KEY=${process.env['OPENAI_API_KEY']}`); } - // copy TAVILY_API_KEY for web search tool - if (process.env['TAVILY_API_KEY']) { - args.push('--env', `TAVILY_API_KEY=${process.env['TAVILY_API_KEY']}`); - } if (process.env['OPENAI_BASE_URL']) { args.push('--env', `OPENAI_BASE_URL=${process.env['OPENAI_BASE_URL']}`); } diff --git a/packages/core/src/agents/runtime/agent-statistics.test.ts b/packages/core/src/agents/runtime/agent-statistics.test.ts index ec9f6e990..fedcecb3d 100644 --- a/packages/core/src/agents/runtime/agent-statistics.test.ts +++ b/packages/core/src/agents/runtime/agent-statistics.test.ts @@ -32,7 +32,7 @@ describe('AgentStatistics', () => { it('should track tool calls', () => { stats.recordToolCall('file_read', true, 100); - stats.recordToolCall('web_search', false, 200, 'Network timeout'); + stats.recordToolCall('web_fetch', false, 200, 'Network timeout'); const summary = stats.getSummary(); expect(summary.totalToolCalls).toBe(2); @@ -81,14 +81,14 @@ describe('AgentStatistics', () => { it('should track individual tool usage', () => { stats.recordToolCall('file_read', true, 100); stats.recordToolCall('file_read', false, 150, 'Permission denied'); - stats.recordToolCall('web_search', true, 300); + stats.recordToolCall('web_fetch', true, 300); const summary = stats.getSummary(); const fileReadTool = summary.toolUsage.find( (t) => t.name === 'file_read', ); - const webSearchTool = summary.toolUsage.find( - (t) => t.name === 'web_search', + const webFetchTool = summary.toolUsage.find( + (t) => t.name === 'web_fetch', ); expect(fileReadTool).toEqual({ @@ -101,8 +101,8 @@ describe('AgentStatistics', () => { averageDurationMs: 125, }); - expect(webSearchTool).toEqual({ - name: 'web_search', + expect(webFetchTool).toEqual({ + name: 'web_fetch', count: 1, success: 1, failure: 0, @@ -153,7 +153,7 @@ describe('AgentStatistics', () => { stats.setRounds(3); stats.recordToolCall('file_read', true, 100); stats.recordToolCall('file_read', true, 150); - stats.recordToolCall('web_search', false, 2000, 'Network timeout'); + stats.recordToolCall('web_fetch', false, 2000, 'Network timeout'); stats.recordTokens(2000, 1000); }); @@ -176,7 +176,7 @@ describe('AgentStatistics', () => { expect(result).toContain('Top tools:'); expect(result).toContain('- file_read: 2 calls (2 ok, 0 fail'); - expect(result).toContain('- web_search: 1 calls (0 ok, 1 fail'); + expect(result).toContain('- web_fetch: 1 calls (0 ok, 1 fail'); expect(result).toContain('last error: Network timeout'); }); @@ -288,7 +288,7 @@ describe('AgentStatistics', () => { }); it('should identify network failures', () => { - stats.recordToolCall('web_search', false, 100, 'Network timeout'); + stats.recordToolCall('web_fetch', false, 100, 'Network timeout'); const result = stats.formatDetailed('Network task'); expect(result).toContain( diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 16a9f05f7..9faf3a39b 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -404,15 +404,6 @@ export interface ConfigParameters { loadMemoryFromIncludeDirectories?: boolean; importFormat?: 'tree' | 'flat'; chatRecording?: boolean; - // Web search providers - webSearch?: { - provider: Array<{ - type: 'tavily' | 'google' | 'dashscope'; - apiKey?: string; - searchEngineId?: string; - }>; - default: string; - }; chatCompression?: ChatCompressionSettings; interactive?: boolean; trustedFolder?: boolean; @@ -645,14 +636,6 @@ export class Config { private readonly chatRecordingEnabled: boolean; private readonly loadMemoryFromIncludeDirectories: boolean = false; private readonly importFormat: 'tree' | 'flat'; - private readonly webSearch?: { - provider: Array<{ - type: 'tavily' | 'google' | 'dashscope'; - apiKey?: string; - searchEngineId?: string; - }>; - default: string; - }; private readonly chatCompression: ChatCompressionSettings | undefined; private readonly interactive: boolean; private readonly trustedFolder: boolean | undefined; @@ -818,8 +801,7 @@ export class Config { this.allowedHttpHookUrls = params.allowedHttpHookUrls ?? []; this.onPersistPermissionRuleCallback = params.onPersistPermissionRule; - // Web search - this.webSearch = params.webSearch; + // (web search removed) this.useRipgrep = params.useRipgrep ?? true; this.useBuiltinRipgrep = params.useBuiltinRipgrep ?? true; this.shouldUseNodePtyShell = @@ -2249,11 +2231,6 @@ export class Config { return this.getNoBrowser() || !shouldAttemptBrowserLaunch(); } - // Web search provider configuration - getWebSearchConfig() { - return this.getBareMode() ? undefined : this.webSearch; - } - getIdeMode(): boolean { return this.ideMode; } @@ -2710,13 +2687,6 @@ export class Config { const { WebFetchTool } = await import('../tools/web-fetch.js'); return new WebFetchTool(this); }); - // Conditionally register web search tool if web search provider is configured - if (this.getWebSearchConfig()) { - await registerLazy(ToolNames.WEB_SEARCH, async () => { - const { WebSearchTool } = await import('../tools/web-search/index.js'); - return new WebSearchTool(this); - }); - } if (this.isLspEnabled() && this.getLspClient()) { await registerLazy(ToolNames.LSP, async () => { const { LspTool } = await import('../tools/lsp.js'); diff --git a/packages/core/src/extension/claude-converter.ts b/packages/core/src/extension/claude-converter.ts index 75d28be7a..0f8600832 100644 --- a/packages/core/src/extension/claude-converter.ts +++ b/packages/core/src/extension/claude-converter.ts @@ -108,7 +108,7 @@ const CLAUDE_TOOLS_MAPPING: Record = { Task: 'Task', TodoWrite: 'TodoWrite', WebFetch: 'WebFetch', - WebSearch: 'WebSearch', + WebSearch: 'None', Write: 'WriteFile', LS: 'ListFiles', }; diff --git a/packages/core/src/followup/speculationToolGate.test.ts b/packages/core/src/followup/speculationToolGate.test.ts index b72874e62..10b4fdd98 100644 --- a/packages/core/src/followup/speculationToolGate.test.ts +++ b/packages/core/src/followup/speculationToolGate.test.ts @@ -130,7 +130,6 @@ describe('speculationToolGate', () => { ToolNames.ASK_USER_QUESTION, ToolNames.EXIT_PLAN_MODE, ToolNames.WEB_FETCH, - ToolNames.WEB_SEARCH, ])('hits boundary for %s', async (toolName) => { const result = await evaluateToolCall( toolName, diff --git a/packages/core/src/followup/speculationToolGate.ts b/packages/core/src/followup/speculationToolGate.ts index 98a4452bb..f7dd402cd 100644 --- a/packages/core/src/followup/speculationToolGate.ts +++ b/packages/core/src/followup/speculationToolGate.ts @@ -48,7 +48,6 @@ const BOUNDARY_TOOLS = new Set([ ToolNames.ASK_USER_QUESTION, ToolNames.EXIT_PLAN_MODE, ToolNames.WEB_FETCH, - ToolNames.WEB_SEARCH, ]); /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 55b920508..80210ee03 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -98,7 +98,6 @@ export * from './tools/sdk-control-client-transport.js'; export * from './tools/modifiable-tool.js'; // Selective re-exports of types/utilities from tool files (avoids loading full tool modules) -export type { WebSearchProviderConfig } from './tools/web-search/types.js'; export { buildSkillLlmContent } from './tools/skill-utils.js'; // Backward-compatible type re-exports for tool classes removed from eager loading. @@ -128,12 +127,6 @@ export type { TodoWriteParams, } from './tools/todoWrite.js'; export type { WebFetchTool, WebFetchToolParams } from './tools/web-fetch.js'; -export type { - WebSearchTool, - WebSearchToolParams, - WebSearchToolResult, - WebSearchConfig, -} from './tools/web-search/index.js'; export type { WriteFileTool, WriteFileToolParams } from './tools/write-file.js'; export type { CronCreateTool, CronCreateParams } from './tools/cron-create.js'; export type { CronListTool, CronListParams } from './tools/cron-list.js'; diff --git a/packages/core/src/permissions/permission-manager.test.ts b/packages/core/src/permissions/permission-manager.test.ts index 52203308c..7196615ba 100644 --- a/packages/core/src/permissions/permission-manager.test.ts +++ b/packages/core/src/permissions/permission-manager.test.ts @@ -1741,10 +1741,6 @@ describe('buildHumanReadableRuleLabel', () => { expect(buildHumanReadableRuleLabel(['Bash'])).toBe('run commands'); }); - it('converts bare WebSearch rule to "search the web"', () => { - expect(buildHumanReadableRuleLabel(['WebSearch'])).toBe('search the web'); - }); - it('converts Read with absolute path specifier', () => { const label = buildHumanReadableRuleLabel(['Read(//Users/mochi/.qwen/**)']); expect(label).toBe('read files in /Users/mochi/.qwen/'); diff --git a/packages/core/src/permissions/permission-manager.ts b/packages/core/src/permissions/permission-manager.ts index a2db685e9..34303fc5d 100644 --- a/packages/core/src/permissions/permission-manager.ts +++ b/packages/core/src/permissions/permission-manager.ts @@ -412,7 +412,6 @@ export class PermissionManager { 'run_shell_command', 'list_directory', 'web_fetch', - 'web_search', 'todo_write', 'save_memory', 'lsp', diff --git a/packages/core/src/permissions/rule-parser.ts b/packages/core/src/permissions/rule-parser.ts index 2f4613533..b034069c5 100644 --- a/packages/core/src/permissions/rule-parser.ts +++ b/packages/core/src/permissions/rule-parser.ts @@ -93,11 +93,6 @@ export const TOOL_NAME_ALIASES: Readonly> = { WebFetch: 'web_fetch', WebFetchTool: 'web_fetch', - // WebSearch tool - web_search: 'web_search', - WebSearch: 'web_search', - WebSearchTool: 'web_search', - // Agent (subagent) tool agent: 'agent', Agent: 'agent', @@ -312,7 +307,6 @@ const CANONICAL_TO_RULE_DISPLAY: Readonly> = { run_shell_command: 'Bash', // Web web_fetch: 'WebFetch', - web_search: 'WebSearch', // Agent / Skill agent: 'Agent', skill: 'Skill', @@ -426,7 +420,6 @@ const DISPLAY_NAME_TO_VERB: Readonly> = { Edit: 'edit files', Bash: 'run commands', WebFetch: 'fetch from', - WebSearch: 'search the web', Agent: 'use agent', Skill: 'use skill', SaveMemory: 'save memory', diff --git a/packages/core/src/services/microcompaction/microcompact.ts b/packages/core/src/services/microcompaction/microcompact.ts index c40653e5e..a740e6234 100644 --- a/packages/core/src/services/microcompaction/microcompact.ts +++ b/packages/core/src/services/microcompaction/microcompact.ts @@ -17,7 +17,6 @@ const COMPACTABLE_TOOLS = new Set([ ToolNames.GREP, ToolNames.GLOB, ToolNames.WEB_FETCH, - ToolNames.WEB_SEARCH, ToolNames.EDIT, ToolNames.WRITE_FILE, ]); diff --git a/packages/core/src/subagents/builtin-agents.ts b/packages/core/src/subagents/builtin-agents.ts index d645548df..8ddd8f2c0 100644 --- a/packages/core/src/subagents/builtin-agents.ts +++ b/packages/core/src/subagents/builtin-agents.ts @@ -92,7 +92,6 @@ Notes: ToolNames.SHELL, ToolNames.LS, ToolNames.WEB_FETCH, - ToolNames.WEB_SEARCH, ToolNames.TODO_WRITE, ToolNames.MEMORY, ToolNames.SKILL, diff --git a/packages/core/src/tools/tool-error.ts b/packages/core/src/tools/tool-error.ts index 96581602f..fe9d2122b 100644 --- a/packages/core/src/tools/tool-error.ts +++ b/packages/core/src/tools/tool-error.ts @@ -64,9 +64,6 @@ export enum ToolErrorType { WEB_FETCH_FALLBACK_FAILED = 'web_fetch_fallback_failed', WEB_FETCH_PROCESSING_ERROR = 'web_fetch_processing_error', - // WebSearch-specific Errors - WEB_SEARCH_FAILED = 'web_search_failed', - // Truncation Errors OUTPUT_TRUNCATED = 'output_truncated', } diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 9edc21508..256722192 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -22,7 +22,6 @@ export const ToolNames = { SKILL: 'skill', EXIT_PLAN_MODE: 'exit_plan_mode', WEB_FETCH: 'web_fetch', - WEB_SEARCH: 'web_search', LS: 'list_directory', LSP: 'lsp', ASK_USER_QUESTION: 'ask_user_question', @@ -49,7 +48,6 @@ export const ToolDisplayNames = { SKILL: 'Skill', EXIT_PLAN_MODE: 'ExitPlanMode', WEB_FETCH: 'WebFetch', - WEB_SEARCH: 'WebSearch', LS: 'ListFiles', LSP: 'Lsp', ASK_USER_QUESTION: 'AskUserQuestion', diff --git a/packages/core/src/tools/web-search/base-provider.ts b/packages/core/src/tools/web-search/base-provider.ts deleted file mode 100644 index a9bdc5b0d..000000000 --- a/packages/core/src/tools/web-search/base-provider.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { WebSearchProvider, WebSearchResult } from './types.js'; - -/** - * Base implementation for web search providers. - * Provides common functionality for error handling. - */ -export abstract class BaseWebSearchProvider implements WebSearchProvider { - abstract readonly name: string; - - /** - * Check if the provider is available (has required configuration). - */ - abstract isAvailable(): boolean; - - /** - * Perform the actual search implementation. - * @param query The search query - * @param signal Abort signal for cancellation - * @returns Promise resolving to search results - */ - protected abstract performSearch( - query: string, - signal: AbortSignal, - ): Promise; - - /** - * Execute a web search with error handling. - * @param query The search query - * @param signal Abort signal for cancellation - * @returns Promise resolving to search results - */ - async search(query: string, signal: AbortSignal): Promise { - if (!this.isAvailable()) { - throw new Error( - `[${this.name}] Provider is not available. Please check your configuration.`, - ); - } - - try { - return await this.performSearch(query, signal); - } catch (error: unknown) { - if ( - error instanceof Error && - error.message.startsWith(`[${this.name}]`) - ) { - throw error; - } - const message = error instanceof Error ? error.message : String(error); - throw new Error(`[${this.name}] Search failed: ${message}`); - } - } -} diff --git a/packages/core/src/tools/web-search/index.test.ts b/packages/core/src/tools/web-search/index.test.ts deleted file mode 100644 index d851ceae5..000000000 --- a/packages/core/src/tools/web-search/index.test.ts +++ /dev/null @@ -1,312 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { WebSearchTool } from './index.js'; -import type { Config } from '../../config/config.js'; -import type { WebSearchConfig } from './types.js'; -import { ApprovalMode } from '../../config/config.js'; - -describe('WebSearchTool', () => { - let mockConfig: Config; - - beforeEach(() => { - vi.resetAllMocks(); - mockConfig = { - getApprovalMode: vi.fn(() => ApprovalMode.AUTO_EDIT), - setApprovalMode: vi.fn(), - getWebSearchConfig: vi.fn(), - } as unknown as Config; - }); - - describe('formatSearchResults', () => { - it('should use answer when available and append sources', async () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'tavily', - apiKey: 'test-key', - }, - ], - default: 'tavily', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - // Mock fetch to return search results with answer - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - query: 'test query', - answer: 'This is a concise answer from the search provider.', - results: [ - { - title: 'Result 1', - url: 'https://example.com/1', - content: 'Content 1', - }, - { - title: 'Result 2', - url: 'https://example.com/2', - content: 'Content 2', - }, - ], - }), - }); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - expect(result.llmContent).toContain( - 'This is a concise answer from the search provider.', - ); - expect(result.llmContent).toContain('Sources:'); - expect(result.llmContent).toContain( - '[1] Result 1 (https://example.com/1)', - ); - expect(result.llmContent).toContain( - '[2] Result 2 (https://example.com/2)', - ); - }); - - it('should build informative summary when answer is not available', async () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'google', - apiKey: 'test-key', - searchEngineId: 'test-engine', - }, - ], - default: 'google', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - // Mock fetch to return search results without answer - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - items: [ - { - title: 'Google Result 1', - link: 'https://example.com/1', - snippet: 'This is a helpful snippet from the first result.', - }, - { - title: 'Google Result 2', - link: 'https://example.com/2', - snippet: 'This is a helpful snippet from the second result.', - }, - ], - }), - }); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - // Should contain formatted results with title, snippet, and source - expect(result.llmContent).toContain('1. **Google Result 1**'); - expect(result.llmContent).toContain( - 'This is a helpful snippet from the first result.', - ); - expect(result.llmContent).toContain('Source: https://example.com/1'); - expect(result.llmContent).toContain('2. **Google Result 2**'); - expect(result.llmContent).toContain( - 'This is a helpful snippet from the second result.', - ); - expect(result.llmContent).toContain('Source: https://example.com/2'); - - // Should include web_fetch hint - expect(result.llmContent).toContain('web_fetch tool'); - }); - - it('should include optional fields when available', async () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'tavily', - apiKey: 'test-key', - }, - ], - default: 'tavily', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - // Mock fetch to return results with score and publishedDate - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - query: 'test query', - results: [ - { - title: 'Result with metadata', - url: 'https://example.com', - content: 'Content with metadata', - score: 0.95, - published_date: '2024-01-15', - }, - ], - }), - }); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - // Should include relevance score - expect(result.llmContent).toContain('Relevance: 95%'); - // Should include published date - expect(result.llmContent).toContain('Published: 2024-01-15'); - }); - - it('should handle empty results gracefully', async () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'google', - apiKey: 'test-key', - searchEngineId: 'test-engine', - }, - ], - default: 'google', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - // Mock fetch to return empty results - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - items: [], - }), - }); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - expect(result.llmContent).toContain('No search results found'); - }); - - it('should limit to top 5 results in fallback mode', async () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'google', - apiKey: 'test-key', - searchEngineId: 'test-engine', - }, - ], - default: 'google', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - // Mock fetch to return 10 results - const items = Array.from({ length: 10 }, (_, i) => ({ - title: `Result ${i + 1}`, - link: `https://example.com/${i + 1}`, - snippet: `Snippet ${i + 1}`, - })); - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ items }), - }); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - // Should only contain first 5 results - expect(result.llmContent).toContain('1. **Result 1**'); - expect(result.llmContent).toContain('5. **Result 5**'); - expect(result.llmContent).not.toContain('6. **Result 6**'); - expect(result.llmContent).not.toContain('10. **Result 10**'); - }); - }); - - describe('validation', () => { - it('should throw validation error when query is empty', () => { - const tool = new WebSearchTool(mockConfig); - expect(() => tool.build({ query: '' })).toThrow( - "The 'query' parameter cannot be empty", - ); - }); - - it('should throw validation error when provider is empty string', () => { - const tool = new WebSearchTool(mockConfig); - expect(() => tool.build({ query: 'test', provider: '' })).toThrow( - "The 'provider' parameter cannot be empty", - ); - }); - }); - - describe('configuration', () => { - it('should return error when web search is not configured', async () => { - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(null); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const result = await invocation.execute(new AbortController().signal); - - expect(result.error?.message).toContain('Web search is disabled'); - expect(result.llmContent).toContain('Web search is disabled'); - }); - - it('should return descriptive message in getDescription when web search is not configured', () => { - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(null); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const description = invocation.getDescription(); - - expect(description).toBe( - ' (Web search is disabled - configure a provider in settings.json)', - ); - }); - - it('should return provider name in getDescription when web search is configured', () => { - const webSearchConfig: WebSearchConfig = { - provider: [ - { - type: 'tavily', - apiKey: 'test-key', - }, - ], - default: 'tavily', - }; - - ( - mockConfig.getWebSearchConfig as ReturnType - ).mockReturnValue(webSearchConfig); - - const tool = new WebSearchTool(mockConfig); - const invocation = tool.build({ query: 'test query' }); - const description = invocation.getDescription(); - - expect(description).toBe(' (Searching the web via tavily)'); - }); - }); -}); diff --git a/packages/core/src/tools/web-search/index.ts b/packages/core/src/tools/web-search/index.ts deleted file mode 100644 index 142dd880f..000000000 --- a/packages/core/src/tools/web-search/index.ts +++ /dev/null @@ -1,352 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { ToolConfirmationOutcome } from '../tools.js'; -import { - BaseDeclarativeTool, - BaseToolInvocation, - Kind, - type ToolInvocation, - type ToolCallConfirmationDetails, - type ToolInfoConfirmationDetails, - type ToolConfirmationPayload, -} from '../tools.js'; -import type { PermissionDecision } from '../../permissions/types.js'; -import { ToolErrorType } from '../tool-error.js'; - -import type { Config } from '../../config/config.js'; -import { getErrorMessage } from '../../utils/errors.js'; -import { createDebugLogger } from '../../utils/debugLogger.js'; -import { buildContentWithSources } from './utils.js'; -import { TavilyProvider } from './providers/tavily-provider.js'; -import { GoogleProvider } from './providers/google-provider.js'; -import { DashScopeProvider } from './providers/dashscope-provider.js'; -import type { - WebSearchToolParams, - WebSearchToolResult, - WebSearchProvider, - WebSearchResultItem, - WebSearchProviderConfig, - DashScopeProviderConfig, -} from './types.js'; -import { ToolNames, ToolDisplayNames } from '../tool-names.js'; - -const debugLogger = createDebugLogger('WEB_SEARCH'); - -class WebSearchToolInvocation extends BaseToolInvocation< - WebSearchToolParams, - WebSearchToolResult -> { - constructor( - private readonly config: Config, - params: WebSearchToolParams, - ) { - super(params); - } - - override getDescription(): string { - const webSearchConfig = this.config.getWebSearchConfig(); - if (!webSearchConfig) { - return ' (Web search is disabled - configure a provider in settings.json)'; - } - const provider = this.params.provider || webSearchConfig.default; - return ` (Searching the web via ${provider})`; - } - - /** - * WebSearch requires confirmation for external network requests. - */ - override async getDefaultPermission(): Promise { - return 'ask'; - } - - /** - * Constructs the web search confirmation details. - */ - override async getConfirmationDetails( - _abortSignal: AbortSignal, - ): Promise { - // Extract the domain for the permission rule. - const permissionRules = [`WebSearch`]; - - const confirmationDetails: ToolInfoConfirmationDetails = { - type: 'info', - title: 'Confirm Web Search', - prompt: `Search the web for: "${this.params.query}"`, - permissionRules, - onConfirm: async ( - _outcome: ToolConfirmationOutcome, - _payload?: ToolConfirmationPayload, - ) => { - // No-op: persistence is handled by coreToolScheduler via PM rules - }, - }; - return confirmationDetails; - } - - /** - * Create a provider instance from configuration. - */ - private createProvider(config: WebSearchProviderConfig): WebSearchProvider { - switch (config.type) { - case 'tavily': - return new TavilyProvider(config); - case 'google': - return new GoogleProvider(config); - case 'dashscope': { - // Pass auth type to DashScope provider for availability check - const authType = this.config.getAuthType(); - const dashscopeConfig: DashScopeProviderConfig = { - ...config, - authType: authType as string | undefined, - }; - return new DashScopeProvider(dashscopeConfig); - } - default: - throw new Error('Unknown provider type'); - } - } - - /** - * Create all configured providers. - */ - private createProviders( - configs: WebSearchProviderConfig[], - ): Map { - const providers = new Map(); - - for (const config of configs) { - try { - const provider = this.createProvider(config); - if (provider.isAvailable()) { - providers.set(config.type, provider); - } - } catch (error) { - debugLogger.warn(`Failed to create ${config.type} provider:`, error); - } - } - - return providers; - } - - /** - * Select the appropriate provider based on configuration and parameters. - * Throws error if provider not found. - */ - private selectProvider( - providers: Map, - requestedProvider?: string, - defaultProvider?: string, - ): WebSearchProvider { - // Use requested provider if specified - if (requestedProvider) { - const provider = providers.get(requestedProvider); - if (!provider) { - const available = Array.from(providers.keys()).join(', '); - throw new Error( - `The specified provider "${requestedProvider}" is not available. Available: ${available}`, - ); - } - return provider; - } - - // Use default provider if specified and available - if (defaultProvider && providers.has(defaultProvider)) { - return providers.get(defaultProvider)!; - } - - // Fallback to first available provider - const firstProvider = providers.values().next().value; - if (!firstProvider) { - throw new Error('No web search providers are available.'); - } - return firstProvider; - } - - /** - * Format search results into a content string. - */ - private formatSearchResults(searchResult: { - answer?: string; - results: WebSearchResultItem[]; - }): { - content: string; - sources: Array<{ title: string; url: string }>; - } { - const sources = searchResult.results.map((r) => ({ - title: r.title, - url: r.url, - })); - - let content = searchResult.answer?.trim() || ''; - - if (!content) { - // Fallback: Build an informative summary with title + snippet + source link - // This provides enough context for the LLM while keeping token usage efficient - content = searchResult.results - .slice(0, 5) // Top 5 results - .map((r, i) => { - const parts = [`${i + 1}. **${r.title}**`]; - - // Include snippet/content if available - if (r.content?.trim()) { - parts.push(` ${r.content.trim()}`); - } - - // Always include the source URL - parts.push(` Source: ${r.url}`); - - // Optionally include relevance score if available - if (r.score !== undefined) { - parts.push(` Relevance: ${(r.score * 100).toFixed(0)}%`); - } - - // Optionally include publish date if available - if (r.publishedDate) { - parts.push(` Published: ${r.publishedDate}`); - } - - return parts.join('\n'); - }) - .join('\n\n'); - - // Add a note about using web_fetch for detailed content - if (content) { - content += - '\n\n*Note: For detailed content from any source above, use the web_fetch tool with the URL.*'; - } - } else { - // When answer is available, append sources section - content = buildContentWithSources(content, sources); - } - - return { content, sources }; - } - - async execute(signal: AbortSignal): Promise { - // Check if web search is configured - const webSearchConfig = this.config.getWebSearchConfig(); - if (!webSearchConfig) { - return { - llmContent: - 'Web search is disabled. Please configure a web search provider in your settings.', - returnDisplay: 'Web search is disabled.', - error: { - message: 'Web search is disabled', - type: ToolErrorType.EXECUTION_FAILED, - }, - }; - } - - try { - // 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 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 ${provider.name}):\n\n${content}`, - returnDisplay: `Search results for "${this.params.query}".`, - sources, - }; - } catch (error: unknown) { - const errorMessage = `Error during web search: ${getErrorMessage(error)}`; - debugLogger.error(errorMessage, error); - return { - llmContent: errorMessage, - returnDisplay: 'Error performing web search.', - error: { - message: errorMessage, - type: ToolErrorType.EXECUTION_FAILED, - }, - }; - } - } -} - -/** - * A tool to perform web searches using configurable providers. - */ -export class WebSearchTool extends BaseDeclarativeTool< - WebSearchToolParams, - WebSearchToolResult -> { - static readonly Name: string = ToolNames.WEB_SEARCH; - - constructor(private readonly config: Config) { - super( - WebSearchTool.Name, - ToolDisplayNames.WEB_SEARCH, - 'Allows searching the web and using results to inform responses. Provides up-to-date information for current events and recent data beyond the training data cutoff. Returns search results formatted with concise answers and source links. Use this tool when accessing information that may be outdated or beyond the knowledge cutoff.', - Kind.Search, - { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query to find information on the web.', - }, - provider: { - type: 'string', - description: - 'Optional provider to use for the search (e.g., "tavily", "google", "dashscope"). IMPORTANT: Only specify this parameter if you explicitly know which provider to use. Otherwise, omit this parameter entirely and let the system automatically select the appropriate provider based on availability and configuration. The system will choose the best available provider automatically.', - }, - }, - 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."; - } - - // Validate provider parameter if provided - if (params.provider !== undefined && params.provider.trim() === '') { - return "The 'provider' parameter cannot be empty if specified."; - } - - return null; - } - - protected createInvocation( - params: WebSearchToolParams, - ): ToolInvocation { - return new WebSearchToolInvocation(this.config, params); - } -} - -// Re-export types for external use -export type { - WebSearchToolParams, - WebSearchToolResult, - WebSearchConfig, - WebSearchProviderConfig, -} from './types.js'; diff --git a/packages/core/src/tools/web-search/providers/dashscope-provider.ts b/packages/core/src/tools/web-search/providers/dashscope-provider.ts deleted file mode 100644 index fce2b49de..000000000 --- a/packages/core/src/tools/web-search/providers/dashscope-provider.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import { promises as fs } from 'node:fs'; -import * as os from 'os'; -import * as path from 'path'; -import { BaseWebSearchProvider } from '../base-provider.js'; -import type { - WebSearchResult, - WebSearchResultItem, - DashScopeProviderConfig, -} from '../types.js'; -import type { QwenCredentials } from '../../../qwen/qwenOAuth2.js'; - -interface DashScopeSearchItem { - _id: string; - snippet: string; - title: string; - url: string; - timestamp: number; - timestamp_format: string; - hostname: string; - hostlogo?: string; - web_main_body?: string; - _score?: number; -} - -interface DashScopeSearchResponse { - headers: Record; - rid: string; - status: number; - message: string | null; - data: { - total: number; - totalDistinct: number; - docs: DashScopeSearchItem[]; - keywords?: string[]; - qpInfos?: Array<{ - query: string; - cleanQuery: string; - sensitive: boolean; - spellchecked: string; - spellcheck: boolean; - tokenized: string[]; - stopWords: string[]; - synonymWords: string[]; - recognitions: unknown[]; - rewrite: string; - operator: string; - }>; - aggs?: unknown; - extras?: Record; - }; - debug?: unknown; - success: boolean; -} - -// File System Configuration -const QWEN_DIR = '.qwen'; -const QWEN_CREDENTIAL_FILENAME = 'oauth_creds.json'; - -/** - * Get the path to the cached OAuth credentials file. - */ -function getQwenCachedCredentialPath(): string { - return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME); -} - -/** - * Load cached Qwen OAuth credentials from disk. - */ -async function loadQwenCredentials(): Promise { - try { - const keyFile = getQwenCachedCredentialPath(); - const creds = await fs.readFile(keyFile, 'utf-8'); - return JSON.parse(creds) as QwenCredentials; - } catch { - return null; - } -} - -/** - * Web search provider using Alibaba Cloud DashScope API. - */ -export class DashScopeProvider extends BaseWebSearchProvider { - readonly name = 'DashScope'; - - constructor(private readonly config: DashScopeProviderConfig) { - super(); - } - - isAvailable(): boolean { - // DashScope provider is only available when auth type is QWEN_OAUTH - // This ensures it's only used when OAuth credentials are available - return this.config.authType === 'qwen-oauth'; - } - - /** - * Get the access token and API endpoint for authentication and web search. - * Tries OAuth credentials first, falls back to apiKey if OAuth is not available. - * Returns both token and endpoint to avoid loading credentials multiple times. - */ - private async getAuthConfig(): Promise<{ - accessToken: string | null; - apiEndpoint: string; - }> { - // Load credentials once - const credentials = await loadQwenCredentials(); - - // Get access token: try OAuth credentials first, fallback to apiKey - let accessToken: string | null = null; - if (credentials?.access_token) { - // Check if token is not expired - if (credentials.expiry_date && credentials.expiry_date > Date.now()) { - accessToken = credentials.access_token; - } - } - if (!accessToken) { - accessToken = this.config.apiKey || null; - } - - // Get API endpoint: use resource_url from credentials - if (!credentials?.resource_url) { - throw new Error( - 'No resource_url found in credentials. Please authenticate using OAuth', - ); - } - - // Normalize the URL: add protocol if missing - const baseUrl = credentials.resource_url.startsWith('http') - ? credentials.resource_url - : `https://${credentials.resource_url}`; - // Remove trailing slash if present - const normalizedBaseUrl = baseUrl.replace(/\/$/, ''); - const apiEndpoint = `${normalizedBaseUrl}/api/v1/indices/plugin/web_search`; - - return { accessToken, apiEndpoint }; - } - - protected async performSearch( - query: string, - signal: AbortSignal, - ): Promise { - // Get access token and API endpoint (loads credentials once) - const { accessToken, apiEndpoint } = await this.getAuthConfig(); - if (!accessToken) { - throw new Error( - 'No access token available. Please authenticate using OAuth', - ); - } - - const requestBody = { - uq: query, - page: 1, - rows: this.config.maxResults || 10, - }; - - const response = await fetch(apiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(requestBody), - signal, - }); - - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new Error( - `API error: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`, - ); - } - - const data = (await response.json()) as DashScopeSearchResponse; - - if (data.status !== 0) { - throw new Error(`API error: ${data.message || 'Unknown error'}`); - } - - const results: WebSearchResultItem[] = (data.data?.docs || []).map( - (item) => ({ - title: item.title, - url: item.url, - content: item.snippet, - score: item._score, - publishedDate: item.timestamp_format, - }), - ); - - return { - query, - results, - }; - } -} diff --git a/packages/core/src/tools/web-search/providers/google-provider.ts b/packages/core/src/tools/web-search/providers/google-provider.ts deleted file mode 100644 index 0293bfdc6..000000000 --- a/packages/core/src/tools/web-search/providers/google-provider.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseWebSearchProvider } from '../base-provider.js'; -import type { - WebSearchResult, - WebSearchResultItem, - GoogleProviderConfig, -} from '../types.js'; - -interface GoogleSearchItem { - title: string; - link: string; - snippet?: string; - displayLink?: string; - formattedUrl?: string; -} - -interface GoogleSearchResponse { - items?: GoogleSearchItem[]; - searchInformation?: { - totalResults?: string; - searchTime?: number; - }; -} - -/** - * Web search provider using Google Custom Search API. - */ -export class GoogleProvider extends BaseWebSearchProvider { - readonly name = 'Google'; - - constructor(private readonly config: GoogleProviderConfig) { - super(); - } - - isAvailable(): boolean { - return !!(this.config.apiKey && this.config.searchEngineId); - } - - protected async performSearch( - query: string, - signal: AbortSignal, - ): Promise { - const params = new URLSearchParams({ - key: this.config.apiKey!, - cx: this.config.searchEngineId!, - q: query, - num: String(this.config.maxResults || 10), - safe: this.config.safeSearch || 'medium', - }); - - if (this.config.language) { - params.append('lr', `lang_${this.config.language}`); - } - - if (this.config.country) { - params.append('cr', `country${this.config.country}`); - } - - const url = `https://www.googleapis.com/customsearch/v1?${params.toString()}`; - - const response = await fetch(url, { - method: 'GET', - signal, - }); - - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new Error( - `API error: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`, - ); - } - - const data = (await response.json()) as GoogleSearchResponse; - - const results: WebSearchResultItem[] = (data.items || []).map((item) => ({ - title: item.title, - url: item.link, - content: item.snippet, - })); - - return { - query, - results, - }; - } -} diff --git a/packages/core/src/tools/web-search/providers/tavily-provider.ts b/packages/core/src/tools/web-search/providers/tavily-provider.ts deleted file mode 100644 index b6284050f..000000000 --- a/packages/core/src/tools/web-search/providers/tavily-provider.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseWebSearchProvider } from '../base-provider.js'; -import type { - WebSearchResult, - WebSearchResultItem, - TavilyProviderConfig, -} from '../types.js'; - -interface TavilyResultItem { - title: string; - url: string; - content?: string; - score?: number; - published_date?: string; -} - -interface TavilySearchResponse { - query: string; - answer?: string; - results: TavilyResultItem[]; -} - -/** - * Web search provider using Tavily API. - */ -export class TavilyProvider extends BaseWebSearchProvider { - readonly name = 'Tavily'; - - constructor(private readonly config: TavilyProviderConfig) { - super(); - } - - isAvailable(): boolean { - return !!this.config.apiKey; - } - - protected async performSearch( - query: string, - signal: AbortSignal, - ): Promise { - const response = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - api_key: this.config.apiKey, - query, - search_depth: this.config.searchDepth || 'advanced', - max_results: this.config.maxResults || 5, - include_answer: this.config.includeAnswer !== false, - }), - signal, - }); - - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new Error( - `API error: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`, - ); - } - - const data = (await response.json()) as TavilySearchResponse; - - const results: WebSearchResultItem[] = (data.results || []).map((r) => ({ - title: r.title, - url: r.url, - content: r.content, - score: r.score, - publishedDate: r.published_date, - })); - - return { - query, - answer: data.answer?.trim(), - results, - }; - } -} diff --git a/packages/core/src/tools/web-search/types.ts b/packages/core/src/tools/web-search/types.ts deleted file mode 100644 index 12368df66..000000000 --- a/packages/core/src/tools/web-search/types.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { ToolResult } from '../tools.js'; - -/** - * Common interface for all web search providers. - */ -export interface WebSearchProvider { - /** - * The name of the provider. - */ - readonly name: string; - - /** - * Whether the provider is available (has required configuration). - */ - isAvailable(): boolean; - - /** - * Perform a web search with the given query. - * @param query The search query - * @param signal Abort signal for cancellation - * @returns Promise resolving to search results - */ - search(query: string, signal: AbortSignal): Promise; -} - -/** - * Result item from a web search. - */ -export interface WebSearchResultItem { - title: string; - url: string; - content?: string; - score?: number; - publishedDate?: string; -} - -/** - * Result from a web search operation. - */ -export interface WebSearchResult { - /** - * The search query that was executed. - */ - query: string; - - /** - * A concise answer if available from the provider. - */ - answer?: string; - - /** - * List of search result items. - */ - results: WebSearchResultItem[]; - - /** - * Provider-specific metadata. - */ - metadata?: Record; -} - -/** - * Extended tool result that includes sources for web search. - */ -export interface WebSearchToolResult extends ToolResult { - sources?: Array<{ title: string; url: string }>; -} - -/** - * Parameters for the WebSearchTool. - */ -export interface WebSearchToolParams { - /** - * The search query. - */ - query: string; - - /** - * Optional provider to use for the search. - * If not specified, the default provider will be used. - */ - provider?: string; -} - -/** - * Configuration for web search providers. - */ -export interface WebSearchConfig { - /** - * List of available providers with their configurations. - */ - provider: WebSearchProviderConfig[]; - - /** - * The default provider to use. - */ - default: string; -} - -/** - * Base configuration for Tavily provider. - */ -export interface TavilyProviderConfig { - type: 'tavily'; - apiKey?: string; - searchDepth?: 'basic' | 'advanced'; - maxResults?: number; - includeAnswer?: boolean; -} - -/** - * Base configuration for Google provider. - */ -export interface GoogleProviderConfig { - type: 'google'; - apiKey?: string; - searchEngineId?: string; - maxResults?: number; - safeSearch?: 'off' | 'medium' | 'high'; - language?: string; - country?: string; -} - -/** - * Base configuration for DashScope provider. - */ -export interface DashScopeProviderConfig { - type: 'dashscope'; - apiKey?: string; - uid?: string; - appId?: string; - maxResults?: number; - scene?: string; - timeout?: number; - /** - * Optional auth type to determine provider availability. - * If set to 'qwen-oauth', the provider will be available. - * If set to other values or undefined, the provider will check auth type dynamically. - */ - authType?: string; -} - -/** - * Discriminated union type for web search provider configurations. - * This ensures type safety when working with different provider configs. - */ -export type WebSearchProviderConfig = - | TavilyProviderConfig - | GoogleProviderConfig - | DashScopeProviderConfig; diff --git a/packages/core/src/tools/web-search/utils.ts b/packages/core/src/tools/web-search/utils.ts deleted file mode 100644 index 4f4f24dbf..000000000 --- a/packages/core/src/tools/web-search/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Utility functions for web search formatting and processing. - */ - -/** - * Build content string with appended sources section. - * @param content Main content text - * @param sources Array of source objects - * @returns Combined content with sources - */ -export function buildContentWithSources( - content: string, - sources: Array<{ title: string; url: string }>, -): string { - if (!sources.length) return content; - const sourceList = sources - .map((s, i) => `[${i + 1}] ${s.title || 'Untitled'} (${s.url})`) - .join('\n'); - return `${content}\n\nSources:\n${sourceList}`; -} - -/** - * Build a concise summary from top search results. - * @param sources Array of source objects - * @param maxResults Maximum number of results to include - * @returns Concise summary string - */ -export function buildSummary( - sources: Array<{ title: string; url: string }>, - maxResults: number = 3, -): string { - return sources - .slice(0, maxResults) - .map((s, i) => `${i + 1}. ${s.title} - ${s.url}`) - .join('\n'); -} diff --git a/packages/sdk-typescript/test/unit/ProcessTransport.test.ts b/packages/sdk-typescript/test/unit/ProcessTransport.test.ts index 94bacb3f4..65bf2cbe2 100644 --- a/packages/sdk-typescript/test/unit/ProcessTransport.test.ts +++ b/packages/sdk-typescript/test/unit/ProcessTransport.test.ts @@ -192,7 +192,7 @@ describe('ProcessTransport', () => { permissionMode: 'auto-edit', maxSessionTurns: 10, coreTools: ['read_file', 'write_file'], - excludeTools: ['web_search'], + excludeTools: ['web_fetch'], authType: 'api-key', }; @@ -214,7 +214,7 @@ describe('ProcessTransport', () => { '--core-tools', 'read_file,write_file', '--exclude-tools', - 'web_search', + 'web_fetch', '--auth-type', 'api-key', ]), diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index a54ddd1a4..d864a44c0 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -710,18 +710,9 @@ "runtimeOutputDir": { "description": "Custom directory for runtime output (temp files, debug logs, session data, todos, etc.). Config files remain at ~/.qwen. Env var QWEN_RUNTIME_DIR takes priority.", "type": "string" - }, - "tavilyApiKey": { - "description": "⚠️ DEPRECATED: Please use webSearch.provider configuration instead. Legacy API key for the Tavily API.", - "type": "string" } } }, - "webSearch": { - "description": "Configuration for web search providers.", - "type": "object", - "additionalProperties": true - }, "agents": { "description": "Settings for multi-agent collaboration features (Arena, Team, Swarm).", "type": "object", diff --git a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx index c83f20faa..14d7376f5 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx @@ -78,8 +78,6 @@ export const getToolCallComponent = ( case 'fetch': case 'web_fetch': case 'webfetch': - case 'web_search': - case 'websearch': return WebFetchToolCall; default: diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.tsx b/packages/webui/src/components/ChatViewer/ChatViewer.tsx index c95aa140e..8263e9246 100644 --- a/packages/webui/src/components/ChatViewer/ChatViewer.tsx +++ b/packages/webui/src/components/ChatViewer/ChatViewer.tsx @@ -187,8 +187,7 @@ function getToolCallComponent(toolCall: BaseToolCallData) { case 'fetch': case 'web_fetch': case 'webfetch': - case 'web_search': - case 'websearch': + case 'web_search': // compatibility alias for legacy persisted tool-call records return WebFetchToolCall; default: return GenericToolCall; diff --git a/packages/webui/src/components/toolcalls/WebFetchToolCall.tsx b/packages/webui/src/components/toolcalls/WebFetchToolCall.tsx index d6ad482cf..5fe27e1ba 100644 --- a/packages/webui/src/components/toolcalls/WebFetchToolCall.tsx +++ b/packages/webui/src/components/toolcalls/WebFetchToolCall.tsx @@ -191,11 +191,6 @@ const WebFetchToolCallImpl: FC = ({ * @param props - Component props * @returns JSX element */ -export const WebFetchToolCall: FC = (props) => { - const normalizedKind = props.toolCall.kind.toLowerCase(); - const variant: WebVariant = - normalizedKind === 'web_search' || normalizedKind === 'websearch' - ? 'search' - : 'fetch'; // 'fetch', 'web_fetch', 'webfetch' all map to 'fetch' - return ; -}; +export const WebFetchToolCall: FC = (props) => ( + +); diff --git a/packages/webui/src/components/toolcalls/labelUtils.test.ts b/packages/webui/src/components/toolcalls/labelUtils.test.ts index 4247b53a2..4f6ae13a7 100644 --- a/packages/webui/src/components/toolcalls/labelUtils.test.ts +++ b/packages/webui/src/components/toolcalls/labelUtils.test.ts @@ -14,9 +14,8 @@ describe('getToolDisplayLabel', () => { expect(getToolDisplayLabel({ kind: 'command' })).toBe('Shell'); }); - it('uses core names for web fetch and web search', () => { + it('uses core names for web fetch', () => { expect(getToolDisplayLabel({ kind: 'web_fetch' })).toBe('WebFetch'); - expect(getToolDisplayLabel({ kind: 'web_search' })).toBe('WebSearch'); }); it('normalizes todo write labels even when older titles are still present', () => { diff --git a/packages/webui/src/components/toolcalls/labelUtils.ts b/packages/webui/src/components/toolcalls/labelUtils.ts index dff7ea745..021dc62c7 100644 --- a/packages/webui/src/components/toolcalls/labelUtils.ts +++ b/packages/webui/src/components/toolcalls/labelUtils.ts @@ -64,9 +64,6 @@ export const getToolDisplayLabel = ({ case 'webfetch': case 'fetch': return 'WebFetch'; - case 'web_search': - case 'websearch': - return 'WebSearch'; case 'grep': case 'grep_search': return 'Grep';