225 lines
6.8 KiB
TypeScript
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();
|