// Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import { IAiLogger } from "../../api.ts"; import { formatError } from "../tool-error.ts"; import { AiTool, IToolArguments, IToolDefinition } from "../tool.ts"; import { google } from "googleapis"; // import SearchService, { SearchServiceError } from "../../services/search.js"; export interface ISearchResult { title: string; link: string; snippet: string; image?: string; position?: number; displayLink?: string; } export interface ISearchOptions { num?: number; siteSearch?: string; dateRestrict?: string; fileType?: string; safe?: "active" | "off"; sort?: "relevance" | "date"; start?: number; } export class GoogleSearchTool extends AiTool { get name(): string { return "search_google"; } get category(): string { return "search"; } public definition: IToolDefinition = { type: "function", function: { name: this.name, description: "Perform a Google search for relevant information on the web.", parameters: { type: "object", properties: { query: { type: "string", description: "The search query string.", }, num_results: { type: "number", description: "Number of search results to return (default: 10, max: 10).", }, siteSearch: { type: "string", description: "Optional site to restrict the search to (e.g. github.com).", }, dateRestrict: { type: "string", description: "Restricts results to documents based on a date range. Examples: d1 (last day), d7 (last week), d30 (last month), d365 (last year).", }, fileType: { type: "string", description: "Restricts results to files of a specified extension. Examples: pdf, doc, xls, ppt.", }, sort: { type: "string", description: "Sort order for results. Values: 'relevance' (default) or 'date'.", enum: ["relevance", "date"], }, start: { type: "number", description: "The index of the first result to return (for pagination). Default: 1.", }, }, required: ["query"], }, }, }; public async execute( args: IToolArguments, logger: IAiLogger, ): Promise { const { query } = args; if (!query || typeof query !== "string" || query.trim().length === 0) { return formatError({ code: "MISSING_PARAMETER", message: "The 'query' parameter is required.", parameter: "query", expected: "A non-empty string containing the search query.", example: 'search_google(query: "latest AI news")', recoveryHint: "Provide a 'query' parameter with your search terms and try again.", }); } logger.debug("performing Google search for user", { args }); try { const { num_results = 10, siteSearch, dateRestrict, fileType, sort, start, } = args; const results = await this.search(query, { num: Math.min(num_results as number, 10), siteSearch: siteSearch as string | undefined, dateRestrict: dateRestrict as string | undefined, fileType: fileType as string | undefined, safe: "active", sort: sort as "relevance" | "date" | undefined, start: start as number | undefined, }); logger.debug("Google search results", { results }); let content = ""; if (results && results.length) { content += `Here are some relevant search results I found:\n\n`; for (const result of results) { const title = JSON.stringify(result.title || "").slice(1, -1); const link = JSON.stringify(result.link || "").slice(1, -1); const snippet = JSON.stringify(result.snippet || "").slice(1, -1); const displayLink = result.displayLink ? JSON.stringify(result.displayLink).slice(1, -1) : ""; content += `Title: ${title}\n`; content += `Link: ${link}\n`; if (displayLink) { content += `Source: ${displayLink}\n`; } content += `Snippet: ${snippet}\n\n`; } } else { content += "No relevant search results found."; } return content; } catch (error: any) { // Generic error handling return formatError({ code: "OPERATION_FAILED", message: `Failed to perform search: ${error.message}`, recoveryHint: "Please try again or check your search query.", }); } } async search( query: string, options: ISearchOptions, ): Promise { const customSearch = google.customsearch({ version: "v1", auth: this.toolbox.env.google.cse.apiKey, }); const params: any = { q: query, cx: this.toolbox.env.google.cse.engineId, num: options.num || 10, }; if (options.siteSearch) { params.siteSearch = options.siteSearch; } if (options.dateRestrict) { params.dateRestrict = options.dateRestrict; } if (options.fileType) { params.fileType = options.fileType; } if (options.safe) { params.safe = options.safe; } if (options.sort) { params.sort = options.sort; } if (options.start) { params.start = options.start; } const response = await customSearch.cse.list(params); const results: ISearchResult[] = []; if (response.data.items) { response.data.items.forEach((item: any, index: number) => { const result: ISearchResult = { title: item.title || "", link: item.link || "", snippet: item.snippet || "", position: item.position || index + 1, displayLink: item.displayLink || "", }; // Extract thumbnail if available if (item.pagemap?.cse_thumbnail?.[0]?.src) { result.image = item.pagemap.cse_thumbnail[0].src; } results.push(result); }); } return results; } /** * Parse Google CSE API errors and provide meaningful messages */ private parseCseError(error: any): string { // Handle Google API error structure if (error.response?.data?.error) { const apiError = error.response.data.error; const statusCode = error.response.status; switch (statusCode) { case 401: formatError({ code: "UNAUTHORIZED", message: `401 Unauthorized - Invalid API key. ${apiError.message || ""}`, }); case 403: return formatError({ code: "FORBIDDEN", message: `403 Forbidden - ${apiError.message || "Access denied"}. Check your Engine ID and API key permissions.`, }); case 429: const retryAfter = error.response.headers?.["retry-after"]; return formatError({ code: "RATE_LIMIT_EXCEEDED", message: `429 Too Many Requests - Rate limit exceeded. ${retryAfter ? `Retry after: ${retryAfter} seconds.` : ""} ${apiError.message || ""}`, }); default: break; } return formatError({ code: "TOOL_EXECUTION_FAILED", message: `HTTP ${statusCode} - ${apiError.message || "Unknown error"}`, }); } // Handle network errors if (error.code === "ENOTFOUND") { return formatError({ code: "NETWORK_ERROR", message: `Network error (code: ${error.code}) - Unable to reach Google API`, }); } // Generic error return formatError({ code: error.code || "UNKNOWN_ERROR", message: error.message || "An unknown error occurred", }); } }