// src/tools/search/google-search.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved import type { ToolDefinition } from "../../lib/ai-client.js"; import SearchService, { SearchServiceError } from "../../services/search.js"; import type { ToolArguments, ToolContext, ToolMetadata, } from "../../lib/tool.js"; import { DtpTool } from "../../lib/tool.js"; import { ChatSessionMode } from "@/models/chat-session.js"; import HostMonitorService from "../../services/host-monitor.js"; class GoogleSearchTool extends DtpTool { get name(): string { return "GoogleSearchTool"; } get slug(): string { return "google-search"; } get metadata(): ToolMetadata { return { name: this.definition.function.name || "search_google", category: "search", tags: ["web", "external", "google"], modes: [ ChatSessionMode.Plan, ChatSessionMode.Build, ChatSessionMode.Test, ChatSessionMode.Ship, ChatSessionMode.Develop, ], }; } public definition: ToolDefinition = { type: "function", function: { name: "search_google", 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 requiresUserConfig(): boolean { return true; } public async checkUserConfig(userId: string): Promise { return await SearchService.userHasCseConfigured(userId); } public async execute( context: ToolContext, args: ToolArguments, ): Promise { const { query } = args; if (!query || typeof query !== "string" || query.trim().length === 0) { return this.error( "MISSING_PARAMETER", "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.", }, ); } // Get user ID from context const userId = context.session.user._id.toString(); this.log.debug("performing Google search for user", { userId, args }); try { const { num_results = 10, siteSearch, dateRestrict, fileType, sort, start, } = args; const results = await SearchService.searchForUser(userId, 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, }); this.log.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."; } const byteCount = Buffer.byteLength(content, "utf-8"); HostMonitorService.toolCall(byteCount); return this.success( { query, resultCount: results?.length ?? 0 }, content, ); } catch (error: any) { // Handle SearchServiceError with detailed error information if (error instanceof SearchServiceError) { const errorDetails = error.details ? `\nDetails: ${JSON.stringify(error.details, null, 2)}` : ""; // Map SearchServiceError codes to ToolErrorCode let toolErrorCode: import("../../lib/tool-error.js").ToolErrorCode = "OPERATION_FAILED"; let recoveryHint: string | undefined; switch (error.code) { case "CSE_NOT_CONFIGURED": toolErrorCode = "PERMISSION_DENIED"; recoveryHint = "Press Ctrl+, to open Account Settings and configure your Google CSE credentials."; break; case "UNAUTHORIZED": case "FORBIDDEN": toolErrorCode = "PERMISSION_DENIED"; recoveryHint = "Your Google CSE credentials may be invalid. Press Ctrl+, to update them in Account Settings."; break; case "RATE_LIMIT_EXCEEDED": toolErrorCode = "RATE_LIMITED"; break; case "NETWORK_ERROR": toolErrorCode = "OPERATION_FAILED"; break; default: toolErrorCode = "OPERATION_FAILED"; } return this.error(toolErrorCode, `${error.message}${errorDetails}`, { recoveryHint, }); } // Generic error handling return this.error( "OPERATION_FAILED", `Failed to perform search: ${error.message}`, { recoveryHint: "Please try again or check your search query.", }, ); } } } export default new GoogleSearchTool();