2025-05-20 11:36:21 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-10-21 11:45:33 -07:00
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
2025-10-10 15:51:24 -04:00
import { WEB_SEARCH_TOOL_NAME } from './tool-names.js' ;
2025-08-26 00:04:53 +02:00
import type { GroundingMetadata } from '@google/genai' ;
import type { ToolInvocation , ToolResult } from './tools.js' ;
import { BaseDeclarativeTool , BaseToolInvocation , Kind } from './tools.js' ;
2025-08-21 14:40:18 -07:00
import { ToolErrorType } from './tool-error.js' ;
2025-05-20 11:36:21 -07:00
import { getErrorMessage } from '../utils/errors.js' ;
2025-09-01 20:28:54 -04:00
import { type Config } from '../config/config.js' ;
2025-08-27 23:22:21 -04:00
import { getResponseText } from '../utils/partUtils.js' ;
2025-09-01 20:28:54 -04:00
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js' ;
2025-05-20 11:36:21 -07:00
interface GroundingChunkWeb {
uri? : string ;
title? : string ;
}
interface GroundingChunkItem {
web? : GroundingChunkWeb ;
// Other properties might exist if needed in the future
}
interface GroundingSupportSegment {
startIndex : number ;
endIndex : number ;
text? : string ; // text is optional as per the example
}
interface GroundingSupportItem {
segment? : GroundingSupportSegment ;
groundingChunkIndices? : number [ ] ;
confidenceScores? : number [ ] ; // Optional as per example
}
/ * *
* 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? : GroundingMetadata extends { groundingChunks : GroundingChunkItem [ ] }
? GroundingMetadata [ 'groundingChunks' ]
: GroundingChunkItem [ ] ;
}
2025-08-14 13:28:33 -07:00
class WebSearchToolInvocation extends BaseToolInvocation <
2025-05-20 11:36:21 -07:00
WebSearchToolParams ,
WebSearchToolResult
> {
2025-08-14 13:28:33 -07:00
constructor (
private readonly config : Config ,
params : WebSearchToolParams ,
2025-10-21 11:45:33 -07:00
messageBus? : MessageBus ,
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-14 13:28:33 -07:00
) {
2025-10-21 11:45:33 -07:00
super ( params , messageBus , _toolName , _toolDisplayName ) ;
2025-05-20 11:36:21 -07:00
}
2025-08-14 13:28:33 -07:00
override getDescription ( ) : string {
return ` Searching the web for: " ${ this . params . query } " ` ;
2025-05-20 11:36:21 -07:00
}
2025-08-14 13:28:33 -07:00
async execute ( signal : AbortSignal ) : Promise < WebSearchToolResult > {
2025-06-02 14:55:51 -07:00
const geminiClient = this . config . getGeminiClient ( ) ;
2025-05-20 11:36:21 -07:00
try {
2025-06-02 14:55:51 -07:00
const response = await geminiClient . generateContent (
2025-08-14 13:28:33 -07:00
[ { role : 'user' , parts : [ { text : this.params.query } ] } ] ,
2025-06-02 14:55:51 -07:00
{ tools : [ { googleSearch : { } } ] } ,
signal ,
2025-09-01 20:28:54 -04:00
DEFAULT_GEMINI_FLASH_MODEL ,
2025-06-02 14:55:51 -07:00
) ;
2025-05-20 11:36:21 -07:00
const responseText = getResponseText ( response ) ;
const groundingMetadata = response . candidates ? . [ 0 ] ? . groundingMetadata ;
const sources = groundingMetadata ? . groundingChunks as
| GroundingChunkItem [ ]
| undefined ;
const groundingSupports = groundingMetadata ? . groundingSupports as
| GroundingSupportItem [ ]
| undefined ;
if ( ! responseText || ! responseText . trim ( ) ) {
return {
2025-08-14 13:28:33 -07:00
llmContent : ` No search results or information found for query: " ${ this . params . query } " ` ,
2025-05-20 11:36:21 -07:00
returnDisplay : 'No information found.' ,
} ;
}
let modifiedResponseText = responseText ;
const sourceListFormatted : string [ ] = [ ] ;
if ( sources && sources . length > 0 ) {
sources . forEach ( ( source : GroundingChunkItem , index : number ) = > {
const title = source . web ? . title || 'Untitled' ;
const uri = source . web ? . uri || 'No URI' ;
sourceListFormatted . push ( ` [ ${ index + 1 } ] ${ title } ( ${ uri } ) ` ) ;
} ) ;
if ( groundingSupports && groundingSupports . length > 0 ) {
const insertions : Array < { index : number ; marker : string } > = [ ] ;
groundingSupports . forEach ( ( support : GroundingSupportItem ) = > {
if ( support . segment && support . groundingChunkIndices ) {
const citationMarker = support . groundingChunkIndices
. map ( ( chunkIndex : number ) = > ` [ ${ chunkIndex + 1 } ] ` )
. join ( '' ) ;
insertions . push ( {
index : support.segment.endIndex ,
marker : citationMarker ,
} ) ;
}
} ) ;
// Sort insertions by index in descending order to avoid shifting subsequent indices
insertions . sort ( ( a , b ) = > b . index - a . index ) ;
2025-08-23 01:09:16 +09:00
// Use TextEncoder/TextDecoder since segment indices are UTF-8 byte positions
const encoder = new TextEncoder ( ) ;
const responseBytes = encoder . encode ( modifiedResponseText ) ;
const parts : Uint8Array [ ] = [ ] ;
let lastIndex = responseBytes . length ;
for ( const ins of insertions ) {
const pos = Math . min ( ins . index , lastIndex ) ;
parts . unshift ( responseBytes . subarray ( pos , lastIndex ) ) ;
parts . unshift ( encoder . encode ( ins . marker ) ) ;
lastIndex = pos ;
}
parts . unshift ( responseBytes . subarray ( 0 , lastIndex ) ) ;
// Concatenate all parts into a single buffer
const totalLength = parts . reduce ( ( sum , part ) = > sum + part . length , 0 ) ;
const finalBytes = new Uint8Array ( totalLength ) ;
let offset = 0 ;
for ( const part of parts ) {
finalBytes . set ( part , offset ) ;
offset += part . length ;
}
modifiedResponseText = new TextDecoder ( ) . decode ( finalBytes ) ;
2025-05-20 11:36:21 -07:00
}
if ( sourceListFormatted . length > 0 ) {
modifiedResponseText +=
2025-08-14 13:28:33 -07:00
'\n\nSources:\n' + sourceListFormatted . join ( '\n' ) ;
2025-05-20 11:36:21 -07:00
}
}
return {
2025-08-14 13:28:33 -07:00
llmContent : ` Web search results for " ${ this . params . query } ": \ n \ n ${ modifiedResponseText } ` ,
returnDisplay : ` Search results for " ${ this . params . query } " returned. ` ,
2025-05-20 11:36:21 -07:00
sources ,
} ;
} catch ( error : unknown ) {
2025-08-14 13:28:33 -07:00
const errorMessage = ` Error during web search for query " ${
this . params . query
} " : $ { getErrorMessage ( error ) } ` ;
2025-05-20 11:36:21 -07:00
console . error ( errorMessage , error ) ;
return {
llmContent : ` Error: ${ errorMessage } ` ,
returnDisplay : ` Error performing web search. ` ,
2025-08-21 14:40:18 -07:00
error : {
message : errorMessage ,
type : ToolErrorType . WEB_SEARCH_FAILED ,
} ,
2025-05-20 11:36:21 -07:00
} ;
}
}
}
2025-08-14 13:28:33 -07:00
/ * *
* A tool to perform web searches using Google Search via the Gemini API .
* /
export class WebSearchTool extends BaseDeclarativeTool <
WebSearchToolParams ,
WebSearchToolResult
> {
2025-10-20 22:35:35 -04:00
static readonly Name = WEB_SEARCH_TOOL_NAME ;
2025-10-21 11:45:33 -07:00
constructor (
private readonly config : Config ,
messageBus? : MessageBus ,
) {
2025-08-14 13:28:33 -07:00
super (
2025-10-20 22:35:35 -04:00
WebSearchTool . Name ,
2025-08-14 13:28:33 -07:00
'GoogleSearch' ,
'Performs a web search using Google Search (via the Gemini API) and returns the results. This tool is useful for finding information on the internet based on a query.' ,
Kind . Search ,
{
type : 'object' ,
properties : {
query : {
type : 'string' ,
description : 'The search query to find information on the web.' ,
} ,
} ,
required : [ 'query' ] ,
} ,
2025-10-21 11:45:33 -07:00
true , // isOutputMarkdown
false , // canUpdateOutput
messageBus ,
2025-08-14 13:28:33 -07: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-08-19 13:55:06 -07:00
protected override validateToolParamValues (
2025-08-14 13:28:33 -07:00
params : WebSearchToolParams ,
) : string | null {
if ( ! params . query || params . query . trim ( ) === '' ) {
return "The 'query' parameter cannot be empty." ;
}
return null ;
}
protected createInvocation (
params : WebSearchToolParams ,
2025-10-21 11:45:33 -07:00
messageBus? : MessageBus ,
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-14 13:28:33 -07:00
) : ToolInvocation < WebSearchToolParams , WebSearchToolResult > {
2025-10-21 11:45:33 -07:00
return new WebSearchToolInvocation (
this . config ,
params ,
messageBus ,
_toolName ,
_toolDisplayName ,
) ;
2025-08-14 13:28:33 -07:00
}
}