2025-04-19 19:45:42 +01:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-05-29 15:02:31 -07:00
import { GoogleGenAI , GroundingMetadata } from '@google/genai' ;
2025-04-19 19:45:42 +01:00
import { SchemaValidator } from '../utils/schemaValidator.js' ;
import { BaseTool , ToolResult } from './tools.js' ;
import { getErrorMessage } from '../utils/errors.js' ;
2025-05-29 15:02:31 -07:00
import { Config } from '../config/config.js' ;
import { getResponseText } from '../utils/generateContentResponseUtilities.js' ;
2025-05-30 10:57:00 -07:00
import { retryWithBackoff } from '../utils/retry.js' ;
2025-05-29 15:02:31 -07:00
// Interfaces for grounding metadata (similar to web-search.ts)
interface GroundingChunkWeb {
uri? : string ;
title? : string ;
}
interface GroundingChunkItem {
web? : GroundingChunkWeb ;
}
interface GroundingSupportSegment {
startIndex : number ;
endIndex : number ;
text? : string ;
}
interface GroundingSupportItem {
segment? : GroundingSupportSegment ;
groundingChunkIndices? : number [ ] ;
}
2025-04-19 19:45:42 +01:00
/ * *
* Parameters for the WebFetch tool
* /
export interface WebFetchToolParams {
/ * *
2025-05-29 15:02:31 -07:00
* The prompt containing URL ( s ) ( up to 20 ) and instructions for processing their content .
2025-04-19 19:45:42 +01:00
* /
2025-05-29 15:02:31 -07:00
prompt : string ;
2025-04-19 19:45:42 +01:00
}
/ * *
2025-05-02 14:39:39 -07:00
* Implementation of the WebFetch tool logic
2025-04-19 19:45:42 +01:00
* /
2025-04-21 10:53:11 -04:00
export class WebFetchTool extends BaseTool < WebFetchToolParams , ToolResult > {
2025-04-19 19:45:42 +01:00
static readonly Name : string = 'web_fetch' ;
2025-05-29 15:02:31 -07:00
private ai : GoogleGenAI ;
private modelName : string ;
constructor ( private readonly config : Config ) {
2025-04-19 19:45:42 +01:00
super (
2025-04-21 10:53:11 -04:00
WebFetchTool . Name ,
'WebFetch' ,
2025-05-29 15:02:31 -07:00
"Processes content from URL(s) embedded in a prompt. Include up to 20 URLs and instructions (e.g., summarize, extract specific data) directly in the 'prompt' parameter." ,
2025-04-19 19:45:42 +01:00
{
properties : {
2025-05-29 15:02:31 -07:00
prompt : {
2025-04-19 19:45:42 +01:00
description :
2025-05-29 15:02:31 -07:00
'A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content (e.g., "Summarize https://example.com/article and extract key points from https://another.com/data"). Must contain as least one URL starting with http:// or https://.' ,
2025-04-19 19:45:42 +01:00
type : 'string' ,
} ,
} ,
2025-05-29 15:02:31 -07:00
required : [ 'prompt' ] ,
2025-04-19 19:45:42 +01:00
type : 'object' ,
} ,
) ;
2025-05-29 15:02:31 -07:00
const apiKeyFromConfig = this . config . getApiKey ( ) ;
this . ai = new GoogleGenAI ( {
apiKey : apiKeyFromConfig === '' ? undefined : apiKeyFromConfig ,
} ) ;
this . modelName = this . config . getModel ( ) ;
2025-04-19 19:45:42 +01:00
}
validateParams ( params : WebFetchToolParams ) : string | null {
if (
this . schema . parameters &&
! SchemaValidator . validate (
this . schema . parameters as Record < string , unknown > ,
params ,
)
) {
return 'Parameters failed schema validation.' ;
}
2025-05-29 15:02:31 -07:00
if ( ! params . prompt || params . prompt . trim ( ) === '' ) {
return "The 'prompt' parameter cannot be empty and must contain URL(s) and instructions." ;
}
if (
! params . prompt . includes ( 'http://' ) &&
! params . prompt . includes ( 'https://' )
) {
return "The 'prompt' must contain at least one valid URL (starting with http:// or https://)." ;
2025-04-19 19:45:42 +01:00
}
return null ;
}
getDescription ( params : WebFetchToolParams ) : string {
2025-05-29 15:02:31 -07:00
const displayPrompt =
params . prompt . length > 100
? params . prompt . substring ( 0 , 97 ) + '...'
: params . prompt ;
return ` Processing URLs and instructions from prompt: " ${ displayPrompt } " ` ;
2025-04-19 19:45:42 +01:00
}
2025-05-09 23:29:02 -07:00
async execute (
params : WebFetchToolParams ,
_signal : AbortSignal ,
) : Promise < ToolResult > {
2025-04-19 19:45:42 +01:00
const validationError = this . validateParams ( params ) ;
if ( validationError ) {
return {
llmContent : ` Error: Invalid parameters provided. Reason: ${ validationError } ` ,
2025-05-29 15:02:31 -07:00
returnDisplay : validationError ,
2025-04-19 19:45:42 +01:00
} ;
}
2025-05-29 15:02:31 -07:00
const userPrompt = params . prompt ;
2025-04-19 19:45:42 +01:00
try {
2025-05-30 10:57:00 -07:00
const apiCall = ( ) = >
this . ai . models . generateContent ( {
model : this.modelName ,
contents : [
{
role : 'user' ,
parts : [ { text : userPrompt } ] ,
} ,
] ,
config : {
tools : [ { urlContext : { } } ] ,
2025-05-29 15:02:31 -07:00
} ,
2025-05-30 10:57:00 -07:00
} ) ;
const response = await retryWithBackoff ( apiCall ) ;
2025-04-19 19:45:42 +01:00
2025-05-29 15:02:31 -07:00
console . debug (
` [WebFetchTool] Full response for prompt " ${ userPrompt . substring ( 0 , 50 ) } ...": ` ,
JSON . stringify ( response , null , 2 ) ,
) ;
let responseText = getResponseText ( response ) || '' ;
const urlContextMeta = response . candidates ? . [ 0 ] ? . urlContextMetadata ;
const groundingMetadata = response . candidates ? . [ 0 ] ? . groundingMetadata as
| GroundingMetadata
| undefined ;
const sources = groundingMetadata ? . groundingChunks as
| GroundingChunkItem [ ]
| undefined ;
const groundingSupports = groundingMetadata ? . groundingSupports as
| GroundingSupportItem [ ]
| undefined ;
// Error Handling
let processingError = false ;
let errorDetail = 'An unknown error occurred during content processing.' ;
if (
urlContextMeta ? . urlMetadata &&
urlContextMeta . urlMetadata . length > 0
) {
const allStatuses = urlContextMeta . urlMetadata . map (
( m ) = > m . urlRetrievalStatus ,
) ;
if ( allStatuses . every ( ( s ) = > s !== 'URL_RETRIEVAL_STATUS_SUCCESS' ) ) {
processingError = true ;
errorDetail = ` All URL retrieval attempts failed. Statuses: ${ allStatuses . join ( ', ' ) } . API reported: " ${ responseText || 'No additional detail.' } " ` ;
}
} else if ( ! responseText . trim ( ) && ! sources ? . length ) {
// No URL metadata and no content/sources
processingError = true ;
errorDetail =
'No content was returned and no URL metadata was available to determine fetch status.' ;
2025-04-19 19:45:42 +01:00
}
if (
2025-05-29 15:02:31 -07:00
! processingError &&
! responseText . trim ( ) &&
( ! sources || sources . length === 0 )
2025-04-19 19:45:42 +01:00
) {
2025-05-29 15:02:31 -07:00
// Successfully retrieved some URL (or no specific error from urlContextMeta), but no usable text or grounding data.
processingError = true ;
errorDetail =
'URL(s) processed, but no substantive content or grounding information was found.' ;
}
if ( processingError ) {
const errorText = ` Failed to process prompt and fetch URL data. ${ errorDetail } ` ;
2025-04-19 19:45:42 +01:00
return {
llmContent : ` Error: ${ errorText } ` ,
returnDisplay : ` Error: ${ errorText } ` ,
} ;
}
2025-05-29 15:02:31 -07:00
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 || 'Unknown URI' ; // Fallback if URI is missing
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 ,
} ) ;
}
} ) ;
insertions . sort ( ( a , b ) = > b . index - a . index ) ;
const responseChars = responseText . split ( '' ) ;
insertions . forEach ( ( insertion ) = > {
responseChars . splice ( insertion . index , 0 , insertion . marker ) ;
} ) ;
responseText = responseChars . join ( '' ) ;
}
if ( sourceListFormatted . length > 0 ) {
responseText += `
Sources :
$ { sourceListFormatted . join ( '\n' ) } ` ;
}
}
const llmContent = responseText ;
2025-04-19 19:45:42 +01:00
2025-05-29 15:02:31 -07:00
console . debug (
` [WebFetchTool] Formatted tool response for prompt " ${ userPrompt } : \ n \ n": ` ,
llmContent ,
) ;
2025-04-19 19:45:42 +01:00
return {
llmContent ,
2025-05-29 15:02:31 -07:00
returnDisplay : ` Content processed from prompt. ` ,
2025-04-19 19:45:42 +01:00
} ;
} catch ( error : unknown ) {
2025-05-29 15:02:31 -07:00
const errorMessage = ` Error processing web content for prompt " ${ userPrompt . substring ( 0 , 50 ) } ...": ${ getErrorMessage ( error ) } ` ;
console . error ( errorMessage , error ) ;
2025-04-19 19:45:42 +01:00
return {
llmContent : ` Error: ${ errorMessage } ` ,
returnDisplay : ` Error: ${ errorMessage } ` ,
} ;
}
}
}