mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502)
* feat(web-search): add GLM (ZhipuAI) web search provider - Add GlmProvider class implementing BaseWebSearchProvider using the ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search) - Support multiple search engines: search_std, search_pro, search_pro_sogou, search_pro_quark - Support optional config: maxResults, searchIntent, searchRecencyFilter, contentSize, searchDomainFilter - Truncate query to 70 characters per API limit - Register 'glm' in the provider discriminated union (types.ts) and createProvider() switch (index.ts) - Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class - Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts - Forward GLM_API_KEY in sandbox environment - Update provider priority list: Tavily > Google > GLM > DashScope - Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts - Update docs/developers/tools/web-search.md with GLM configuration, env vars, CLI args, pricing, and corrected DashScope billing info - Fix stale OAuth/free-tier references in web-search.md Closes #3496 * docs(web-search): fix DashScope note and add GLM server-side limitations * fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency - DashScopeProvider.isAvailable() now checks config.apiKey instead of authType - Remove OAuth credential file reading and resource_url requirement - Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search - Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag - Forward DASHSCOPE_API_KEY into sandbox environment - Update integration test to detect DASHSCOPE_API_KEY - Update docs to reflect new API key based configuration * feat(web-search): remove built-in web search tool The web_search tool and all related provider implementations are removed. Web search functionality will be provided via MCP integrations instead, which is the direction the broader agent ecosystem is moving. Removed: - packages/core/src/tools/web-search/ (entire directory) - packages/cli/src/config/webSearch.ts - integration-tests/cli/web_search.test.ts - ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED - webSearch config in ConfigParams, Config class, settingsSchema - CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id, --glm-api-key, --dashscope-api-key, --web-search-default - Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys - web_search from rule-parser, permission-manager, speculation gate, microcompact tool set, and builtin-agents tool list * fix: remove websearch reference * docs: remove websearch tool * docs: add break change guide * fix review
This commit is contained in:
parent
3e74a33460
commit
aeeb2976d6
45 changed files with 179 additions and 1842 deletions
|
|
@ -18,7 +18,6 @@ tools:
|
|||
- run_shell_command
|
||||
- skill
|
||||
- web_fetch
|
||||
- web_search
|
||||
---
|
||||
|
||||
# Test Engineer — Bug Reproduction & Verification
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ tools:
|
|||
- read_file
|
||||
- write_file
|
||||
- read_many_files
|
||||
- web_search
|
||||
---
|
||||
|
||||
You are a technical documentation specialist.
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {};
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -905,7 +905,6 @@ describe('Permission Control (E2E)', () => {
|
|||
'grep_search',
|
||||
'glob',
|
||||
'list_directory',
|
||||
'web_search',
|
||||
'web_fetch',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ tools:
|
|||
- NotebookRead
|
||||
- WebFetch
|
||||
- TodoWrite
|
||||
- WebSearch
|
||||
modelConfig:
|
||||
model: qwen3-coder-plus
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CliArgs> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ describe('SettingsSchema', () => {
|
|||
'mcp',
|
||||
'security',
|
||||
'advanced',
|
||||
'webSearch',
|
||||
];
|
||||
|
||||
expectedSettings.forEach((setting) => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const CLAUDE_TOOLS_MAPPING: Record<string, string | string[]> = {
|
|||
Task: 'Task',
|
||||
TodoWrite: 'TodoWrite',
|
||||
WebFetch: 'WebFetch',
|
||||
WebSearch: 'WebSearch',
|
||||
WebSearch: 'None',
|
||||
Write: 'WriteFile',
|
||||
LS: 'ListFiles',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ const BOUNDARY_TOOLS = new Set<string>([
|
|||
ToolNames.ASK_USER_QUESTION,
|
||||
ToolNames.EXIT_PLAN_MODE,
|
||||
ToolNames.WEB_FETCH,
|
||||
ToolNames.WEB_SEARCH,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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/');
|
||||
|
|
|
|||
|
|
@ -412,7 +412,6 @@ export class PermissionManager {
|
|||
'run_shell_command',
|
||||
'list_directory',
|
||||
'web_fetch',
|
||||
'web_search',
|
||||
'todo_write',
|
||||
'save_memory',
|
||||
'lsp',
|
||||
|
|
|
|||
|
|
@ -93,11 +93,6 @@ export const TOOL_NAME_ALIASES: Readonly<Record<string, string>> = {
|
|||
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<Record<string, string>> = {
|
|||
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<Record<string, string>> = {
|
|||
Edit: 'edit files',
|
||||
Bash: 'run commands',
|
||||
WebFetch: 'fetch from',
|
||||
WebSearch: 'search the web',
|
||||
Agent: 'use agent',
|
||||
Skill: 'use skill',
|
||||
SaveMemory: 'save memory',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ const COMPACTABLE_TOOLS = new Set<string>([
|
|||
ToolNames.GREP,
|
||||
ToolNames.GLOB,
|
||||
ToolNames.WEB_FETCH,
|
||||
ToolNames.WEB_SEARCH,
|
||||
ToolNames.EDIT,
|
||||
ToolNames.WRITE_FILE,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ Notes:
|
|||
ToolNames.SHELL,
|
||||
ToolNames.LS,
|
||||
ToolNames.WEB_FETCH,
|
||||
ToolNames.WEB_SEARCH,
|
||||
ToolNames.TODO_WRITE,
|
||||
ToolNames.MEMORY,
|
||||
ToolNames.SKILL,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<WebSearchResult>;
|
||||
|
||||
/**
|
||||
* 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<WebSearchResult> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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<typeof vi.fn>
|
||||
).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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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<PermissionDecision> {
|
||||
return 'ask';
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the web search confirmation details.
|
||||
*/
|
||||
override async getConfirmationDetails(
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolCallConfirmationDetails> {
|
||||
// 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<string, WebSearchProvider> {
|
||||
const providers = new Map<string, WebSearchProvider>();
|
||||
|
||||
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<string, WebSearchProvider>,
|
||||
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<WebSearchToolResult> {
|
||||
// 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<WebSearchToolParams, WebSearchToolResult> {
|
||||
return new WebSearchToolInvocation(this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export types for external use
|
||||
export type {
|
||||
WebSearchToolParams,
|
||||
WebSearchToolResult,
|
||||
WebSearchConfig,
|
||||
WebSearchProviderConfig,
|
||||
} from './types.js';
|
||||
|
|
@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
};
|
||||
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<QwenCredentials | null> {
|
||||
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<WebSearchResult> {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WebSearchResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WebSearchResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WebSearchResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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',
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -78,8 +78,6 @@ export const getToolCallComponent = (
|
|||
case 'fetch':
|
||||
case 'web_fetch':
|
||||
case 'webfetch':
|
||||
case 'web_search':
|
||||
case 'websearch':
|
||||
return WebFetchToolCall;
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -191,11 +191,6 @@ const WebFetchToolCallImpl: FC<BaseToolCallProps & { variant: WebVariant }> = ({
|
|||
* @param props - Component props
|
||||
* @returns JSX element
|
||||
*/
|
||||
export const WebFetchToolCall: FC<BaseToolCallProps> = (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 <WebFetchToolCallImpl {...props} variant={variant} />;
|
||||
};
|
||||
export const WebFetchToolCall: FC<BaseToolCallProps> = (props) => (
|
||||
<WebFetchToolCallImpl {...props} variant={'fetch'} />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue