| | |
| | |
| | |
| | |
| | |
| |
|
| | import { GroundingMetadata } from '@google/genai'; |
| | import { BaseTool, ToolResult } from './tools.js'; |
| | import { SchemaValidator } from '../utils/schemaValidator.js'; |
| |
|
| | import { getErrorMessage } from '../utils/errors.js'; |
| | import { Config } from '../config/config.js'; |
| | import { getResponseText } from '../utils/generateContentResponseUtilities.js'; |
| |
|
| | interface GroundingChunkWeb { |
| | uri?: string; |
| | title?: string; |
| | } |
| |
|
| | interface GroundingChunkItem { |
| | web?: GroundingChunkWeb; |
| | |
| | } |
| |
|
| | interface GroundingSupportSegment { |
| | startIndex: number; |
| | endIndex: number; |
| | text?: string; |
| | } |
| |
|
| | interface GroundingSupportItem { |
| | segment?: GroundingSupportSegment; |
| | groundingChunkIndices?: number[]; |
| | confidenceScores?: number[]; |
| | } |
| |
|
| | |
| | |
| | |
| | export interface WebSearchToolParams { |
| | |
| | |
| | |
| |
|
| | query: string; |
| | } |
| |
|
| | |
| | |
| | |
| | export interface WebSearchToolResult extends ToolResult { |
| | sources?: GroundingMetadata extends { groundingChunks: GroundingChunkItem[] } |
| | ? GroundingMetadata['groundingChunks'] |
| | : GroundingChunkItem[]; |
| | } |
| |
|
| | |
| | |
| | |
| | export class WebSearchTool extends BaseTool< |
| | WebSearchToolParams, |
| | WebSearchToolResult |
| | > { |
| | static readonly Name: string = 'google_web_search'; |
| |
|
| | constructor(private readonly config: Config) { |
| | super( |
| | WebSearchTool.Name, |
| | '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.', |
| | { |
| | type: 'object', |
| | properties: { |
| | query: { |
| | type: 'string', |
| | description: 'The search query to find information on the web.', |
| | }, |
| | }, |
| | required: ['query'], |
| | }, |
| | ); |
| | } |
| |
|
| | validateParams(params: WebSearchToolParams): string | null { |
| | if ( |
| | this.schema.parameters && |
| | !SchemaValidator.validate( |
| | this.schema.parameters as Record<string, unknown>, |
| | params, |
| | ) |
| | ) { |
| | return "Parameters failed schema validation. Ensure 'query' is a string."; |
| | } |
| | 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}"`; |
| | } |
| |
|
| | async execute( |
| | params: WebSearchToolParams, |
| | signal: AbortSignal, |
| | ): Promise<WebSearchToolResult> { |
| | const validationError = this.validateParams(params); |
| | if (validationError) { |
| | return { |
| | llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, |
| | returnDisplay: validationError, |
| | }; |
| | } |
| | const geminiClient = this.config.getGeminiClient(); |
| |
|
| | try { |
| | const response = await geminiClient.generateContent( |
| | [{ role: 'user', parts: [{ text: params.query }] }], |
| | { tools: [{ googleSearch: {} }] }, |
| | signal, |
| | ); |
| |
|
| | 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 { |
| | llmContent: `No search results or information found for query: "${params.query}"`, |
| | 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, |
| | }); |
| | } |
| | }); |
| |
|
| | |
| | insertions.sort((a, b) => b.index - a.index); |
| |
|
| | const responseChars = modifiedResponseText.split(''); |
| | insertions.forEach((insertion) => { |
| | |
| | responseChars.splice(insertion.index, 0, insertion.marker); |
| | }); |
| | modifiedResponseText = responseChars.join(''); |
| | } |
| |
|
| | if (sourceListFormatted.length > 0) { |
| | modifiedResponseText += |
| | '\n\nSources:\n' + sourceListFormatted.join('\n'); |
| | } |
| | } |
| |
|
| | return { |
| | llmContent: `Web search results for "${params.query}":\n\n${modifiedResponseText}`, |
| | returnDisplay: `Search results for "${params.query}" returned.`, |
| | sources, |
| | }; |
| | } catch (error: unknown) { |
| | const errorMessage = `Error during web search for query "${params.query}": ${getErrorMessage(error)}`; |
| | console.error(errorMessage, error); |
| | return { |
| | llmContent: `Error: ${errorMessage}`, |
| | returnDisplay: `Error performing web search.`, |
| | }; |
| | } |
| | } |
| | } |
| |
|