gadget/docs/archive/tools/search/google.ts

225 lines
6.8 KiB
TypeScript

// 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<boolean> {
return await SearchService.userHasCseConfigured(userId);
}
public async execute(
context: ToolContext,
args: ToolArguments,
): Promise<string> {
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();