/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { BaseDeclarativeTool, BaseToolInvocation, Kind, ToolInvocation, ToolResult, } from './tools.js'; import { getErrorMessage } from '../utils/errors.js'; import { Config } from '../config/config.js'; interface TavilyResultItem { title: string; url: string; content?: string; score?: number; published_date?: string; } interface TavilySearchResponse { query: string; answer?: string; results: TavilyResultItem[]; } /** * Parameters for the WebSearchTool. */ export interface WebSearchToolParams { /** * The search query. */ query: string; } /** * Extends ToolResult to include sources for web search. */ export interface WebSearchToolResult extends ToolResult { sources?: Array<{ title: string; url: string }>; } class WebSearchToolInvocation extends BaseToolInvocation< WebSearchToolParams, WebSearchToolResult > { constructor( private readonly config: Config, params: WebSearchToolParams, ) { super(params); } override getDescription(): string { return `Searching the web for: "${this.params.query}"`; } async execute(signal: AbortSignal): Promise { const apiKey = this.config.getTavilyApiKey() || process.env['TAVILY_API_KEY']; if (!apiKey) { return { llmContent: 'Web search is disabled because TAVILY_API_KEY is not configured. Please set it in your settings.json, .env file, or via --tavily-api-key command line argument to enable web search.', returnDisplay: 'Web search disabled. Configure TAVILY_API_KEY to enable Tavily search.', }; } try { const response = await fetch('https://api.tavily.com/search', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ api_key: apiKey, query: this.params.query, search_depth: 'advanced', max_results: 5, include_answer: true, }), signal, }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error( `Tavily API error: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`, ); } const data = (await response.json()) as TavilySearchResponse; const sources = (data.results || []).map((r) => ({ title: r.title, url: r.url, })); const sourceListFormatted = sources.map( (s, i) => `[${i + 1}] ${s.title || 'Untitled'} (${s.url})`, ); let content = data.answer?.trim() || ''; if (!content) { // Fallback: build a concise summary from top results content = sources .slice(0, 3) .map((s, i) => `${i + 1}. ${s.title} - ${s.url}`) .join('\n'); } if (sourceListFormatted.length > 0) { content += `\n\nSources:\n${sourceListFormatted.join('\n')}`; } if (!content.trim()) { return { llmContent: `No search results or information found for query: "${this.params.query}"`, returnDisplay: 'No information found.', }; } return { llmContent: `Web search results for "${this.params.query}":\n\n${content}`, returnDisplay: `Search results for "${this.params.query}" returned.`, sources, }; } catch (error: unknown) { const errorMessage = `Error during web search for query "${this.params.query}": ${getErrorMessage( error, )}`; console.error(errorMessage, error); return { llmContent: `Error: ${errorMessage}`, returnDisplay: `Error performing web search.`, }; } } } /** * A tool to perform web searches using Google Search via the Gemini API. */ export class WebSearchTool extends BaseDeclarativeTool< WebSearchToolParams, WebSearchToolResult > { static readonly Name: string = 'web_search'; constructor(private readonly config: Config) { super( WebSearchTool.Name, 'WebSearch', 'Performs a web search using the Tavily API and returns a concise answer with sources. Requires the TAVILY_API_KEY environment variable.', Kind.Search, { type: 'object', properties: { query: { type: 'string', description: 'The search query to find information on the web.', }, }, required: ['query'], }, ); } /** * Validates the parameters for the WebSearchTool. * @param params The parameters to validate * @returns An error message string if validation fails, null if valid */ protected override validateToolParamValues( params: WebSearchToolParams, ): string | null { if (!params.query || params.query.trim() === '') { return "The 'query' parameter cannot be empty."; } return null; } protected createInvocation( params: WebSearchToolParams, ): ToolInvocation { return new WebSearchToolInvocation(this.config, params); } }