2025-05-21 02:36:21 +08:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-07-18 06:25:23 +08:00
import { BaseTool , Icon , ToolResult } from './tools.js' ;
2025-05-21 02:36:21 +08:00
import { SchemaValidator } from '../utils/schemaValidator.js' ;
import { getErrorMessage } from '../utils/errors.js' ;
import { Config } from '../config/config.js' ;
2025-08-14 21:20:23 +08:00
interface TavilyResultItem {
title : string ;
url : string ;
content? : string ;
score? : number ;
published_date? : string ;
2025-05-21 02:36:21 +08:00
}
2025-08-14 21:20:23 +08:00
interface TavilySearchResponse {
query : string ;
answer? : string ;
results : TavilyResultItem [ ] ;
2025-05-21 02:36:21 +08:00
}
/ * *
* 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 {
2025-08-14 21:20:23 +08:00
sources? : Array < { title : string ; url : string } > ;
2025-05-21 02:36:21 +08:00
}
/ * *
2025-08-14 21:20:23 +08:00
* A tool to perform web searches using Tavily API .
2025-05-21 02:36:21 +08:00
* /
export class WebSearchTool extends BaseTool <
WebSearchToolParams ,
WebSearchToolResult
> {
2025-08-14 21:20:23 +08:00
static readonly Name : string = 'web_search' ;
2025-05-21 02:36:21 +08:00
constructor ( private readonly config : Config ) {
super (
WebSearchTool . Name ,
2025-08-14 21:20:23 +08:00
'TavilySearch' ,
'Performs a web search using the Tavily API and returns a concise answer with sources. Requires the TAVILY_API_KEY environment variable.' ,
2025-07-18 06:25:23 +08:00
Icon . Globe ,
2025-05-21 02:36:21 +08:00
{
2025-08-22 15:17:42 +08:00
type : 'object' ,
2025-05-21 02:36:21 +08:00
properties : {
query : {
2025-08-22 15:17:42 +08:00
type : 'string' ,
2025-05-21 02:36:21 +08:00
description : 'The search query to find information on the web.' ,
} ,
} ,
required : [ 'query' ] ,
} ,
) ;
}
2025-07-04 08:13:02 +08:00
/ * *
* Validates the parameters for the WebSearchTool .
* @param params The parameters to validate
* @returns An error message string if validation fails , null if valid
* /
2025-07-08 14:48:44 +08:00
validateParams ( params : WebSearchToolParams ) : string | null {
2025-08-12 07:12:41 +08:00
const errors = SchemaValidator . validate (
this . schema . parametersJsonSchema ,
params ,
) ;
2025-07-08 14:48:44 +08:00
if ( errors ) {
return errors ;
2025-05-21 02:36:21 +08:00
}
2025-07-08 14:48:44 +08:00
2025-05-21 02:36:21 +08:00
if ( ! params . query || params . query . trim ( ) === '' ) {
return "The 'query' parameter cannot be empty." ;
}
return null ;
}
getDescription ( params : WebSearchToolParams ) : string {
return ` Searching the web for: " ${ params . query } " ` ;
}
2025-06-03 05:55:51 +08:00
async execute (
params : WebSearchToolParams ,
2025-08-14 21:20:23 +08:00
_signal : AbortSignal ,
2025-06-03 05:55:51 +08:00
) : Promise < WebSearchToolResult > {
2025-07-04 08:13:02 +08:00
const validationError = this . validateToolParams ( params ) ;
2025-05-21 02:36:21 +08:00
if ( validationError ) {
return {
llmContent : ` Error: Invalid parameters provided. Reason: ${ validationError } ` ,
returnDisplay : validationError ,
} ;
}
2025-08-14 21:20:23 +08:00
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.' ,
} ;
}
2025-05-21 02:36:21 +08:00
try {
2025-08-14 21:20:23 +08:00
const controller = new AbortController ( ) ;
const timeoutId = setTimeout ( ( ) = > controller . abort ( ) , 15000 ) ;
const response = await fetch ( 'https://api.tavily.com/search' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON.stringify ( {
api_key : apiKey ,
query : params.query ,
search_depth : 'advanced' ,
max_results : 5 ,
include_answer : true ,
} ) ,
signal : controller.signal ,
} ) ;
clearTimeout ( timeoutId ) ;
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 } ) ` ,
2025-06-03 05:55:51 +08:00
) ;
2025-05-21 02:36:21 +08:00
2025-08-14 21:20:23 +08:00
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' ) } ` ;
}
2025-05-21 02:36:21 +08:00
2025-08-14 21:20:23 +08:00
if ( ! content . trim ( ) ) {
2025-05-21 02:36:21 +08:00
return {
llmContent : ` No search results or information found for query: " ${ params . query } " ` ,
returnDisplay : 'No information found.' ,
} ;
}
return {
2025-08-14 21:20:23 +08:00
llmContent : ` Web search results for " ${ params . query } ": \ n \ n ${ content } ` ,
2025-05-21 02:36:21 +08:00
returnDisplay : ` Search results for " ${ params . query } " returned. ` ,
sources ,
} ;
} catch ( error : unknown ) {
2025-08-14 21:20:23 +08:00
const errorMessage = ` Error during web search for query " ${ params . query } ": ${ getErrorMessage (
error ,
) } ` ;
2025-05-21 02:36:21 +08:00
console . error ( errorMessage , error ) ;
return {
llmContent : ` Error: ${ errorMessage } ` ,
returnDisplay : ` Error performing web search. ` ,
} ;
}
}
}