From 5a7173b01b9f081ad97ee14c6b5de0d2c0373c17 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 4 Sep 2025 17:51:33 +0000 Subject: [PATCH] feat: add web_search tool for all models - Created web_search tool that works for all models, not just those with native web search - Added tool description, handler, and integration with presentAssistantMessage - Added web_search to tool types, groups, and definitions - Implemented mock search results for demonstration (can be replaced with real API) - Added comprehensive tests for the web_search tool Fixes #7675 --- packages/types/src/tool.ts | 1 + .../presentAssistantMessage.ts | 6 + src/core/prompts/tools/index.ts | 3 + src/core/prompts/tools/web-search.ts | 22 +++ .../tools/__tests__/webSearchTool.test.ts | 139 ++++++++++++++++++ src/core/tools/webSearchTool.ts | 95 ++++++++++++ src/shared/tools.ts | 8 + 7 files changed, 274 insertions(+) create mode 100644 src/core/prompts/tools/web-search.ts create mode 100644 src/core/tools/__tests__/webSearchTool.test.ts create mode 100644 src/core/tools/webSearchTool.ts diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 2c7495e5eb..c032c293d4 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -36,6 +36,7 @@ export const toolNames = [ "update_todo_list", "run_slash_command", "generate_image", + "web_search", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 689675999f..bbc278eebb 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -30,6 +30,7 @@ import { newTaskTool } from "../tools/newTaskTool" import { updateTodoListTool } from "../tools/updateTodoListTool" import { runSlashCommandTool } from "../tools/runSlashCommandTool" import { generateImageTool } from "../tools/generateImageTool" +import { webSearchTool } from "../tools/webSearchTool" import { formatResponse } from "../prompts/responses" import { validateToolUse } from "../tools/validateToolUse" @@ -227,6 +228,8 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` case "generate_image": return `[${block.name} for '${block.params.path}']` + case "web_search": + return `[${block.name} for '${block.params.query}']` } } @@ -558,6 +561,9 @@ export async function presentAssistantMessage(cline: Task) { case "generate_image": await generateImageTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break + case "web_search": + await webSearchTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + break } break diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index c212b18a3d..a29fc3e8e2 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -27,6 +27,7 @@ import { getCodebaseSearchDescription } from "./codebase-search" import { getUpdateTodoListDescription } from "./update-todo-list" import { getRunSlashCommandDescription } from "./run-slash-command" import { getGenerateImageDescription } from "./generate-image" +import { getWebSearchDescription } from "./web-search" import { CodeIndexManager } from "../../../services/code-index/manager" // Map of tool names to their description functions @@ -60,6 +61,7 @@ const toolDescriptionMap: Record string | undefined> update_todo_list: (args) => getUpdateTodoListDescription(args), run_slash_command: () => getRunSlashCommandDescription(), generate_image: (args) => getGenerateImageDescription(args), + web_search: () => getWebSearchDescription(), } export function getToolDescriptionsForMode( @@ -180,4 +182,5 @@ export { getCodebaseSearchDescription, getRunSlashCommandDescription, getGenerateImageDescription, + getWebSearchDescription, } diff --git a/src/core/prompts/tools/web-search.ts b/src/core/prompts/tools/web-search.ts new file mode 100644 index 0000000000..219743aeb8 --- /dev/null +++ b/src/core/prompts/tools/web-search.ts @@ -0,0 +1,22 @@ +export function getWebSearchDescription(): string { + return `## web_search +Description: Request to perform a web search and retrieve relevant information from the internet. This tool allows you to search for current information, documentation, tutorials, and other web content that may be helpful for completing tasks. +Parameters: +- query: (required) The search query string. Be specific and include relevant keywords for better results. +Usage: + +Your search query here + + +Example: Searching for Chrome extension development documentation + +Chrome extension development manifest v3 documentation + + +Example: Searching for a specific error message + +"TypeError: Cannot read property" React hooks solution + + +Note: This tool performs a web search and returns summarized results. The quality of results depends on the specificity of your query.` +} diff --git a/src/core/tools/__tests__/webSearchTool.test.ts b/src/core/tools/__tests__/webSearchTool.test.ts new file mode 100644 index 0000000000..b788d86b99 --- /dev/null +++ b/src/core/tools/__tests__/webSearchTool.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { webSearchTool } from "../webSearchTool" +import { Task } from "../../task/Task" +import { formatResponse } from "../../prompts/responses" + +describe("webSearchTool", () => { + let mockCline: any + let mockBlock: any + let mockAskApproval: any + let mockHandleError: any + let mockPushToolResult: any + let mockRemoveClosingTag: any + + beforeEach(() => { + // Create mock Task instance + mockCline = { + consecutiveMistakeCount: 0, + recordToolError: vi.fn(), + recordToolUsage: vi.fn(), + sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"), + say: vi.fn().mockResolvedValue(undefined), + } + + // Create mock block + mockBlock = { + name: "web_search", + params: { + query: "test search query", + }, + partial: false, + } + + // Create mock functions + mockAskApproval = vi.fn().mockResolvedValue(true) + mockHandleError = vi.fn() + mockPushToolResult = vi.fn() + mockRemoveClosingTag = vi.fn((tag, text) => text || "") + }) + + it("should handle missing query parameter", async () => { + mockBlock.params.query = undefined + + await webSearchTool( + mockCline as unknown as Task, + mockBlock, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("web_search") + expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("web_search", "query") + expect(mockPushToolResult).toHaveBeenCalledWith("Missing parameter error") + }) + + it("should skip execution when block is partial", async () => { + mockBlock.partial = true + + await webSearchTool( + mockCline as unknown as Task, + mockBlock, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockAskApproval).not.toHaveBeenCalled() + expect(mockPushToolResult).not.toHaveBeenCalled() + }) + + it("should handle user rejection", async () => { + mockAskApproval.mockResolvedValue(false) + + await webSearchTool( + mockCline as unknown as Task, + mockBlock, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockAskApproval).toHaveBeenCalled() + expect(mockPushToolResult).not.toHaveBeenCalled() + expect(mockCline.recordToolUsage).not.toHaveBeenCalled() + }) + + it("should perform search and return results when approved", async () => { + await webSearchTool( + mockCline as unknown as Task, + mockBlock, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Verify approval was requested + expect(mockAskApproval).toHaveBeenCalledWith( + "tool", + JSON.stringify({ + tool: "webSearch", + query: "test search query", + }), + ) + + // Verify search was logged + expect(mockCline.say).toHaveBeenCalledWith("text", 'Searching the web for: "test search query"') + + // Verify tool usage was recorded + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("web_search") + + // Verify results were pushed + expect(mockPushToolResult).toHaveBeenCalled() + const resultCall = mockPushToolResult.mock.calls[0][0] + expect(resultCall).toContain("Web search results") + expect(resultCall).toContain("test search query") + }) + + it("should handle errors during search", async () => { + const testError = new Error("Search failed") + mockCline.say.mockRejectedValueOnce(testError) + + await webSearchTool( + mockCline as unknown as Task, + mockBlock, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockHandleError).toHaveBeenCalledWith("performing web search", testError) + expect(mockCline.recordToolError).toHaveBeenCalledWith("web_search") + }) +}) diff --git a/src/core/tools/webSearchTool.ts b/src/core/tools/webSearchTool.ts new file mode 100644 index 0000000000..d4972eb0ad --- /dev/null +++ b/src/core/tools/webSearchTool.ts @@ -0,0 +1,95 @@ +import { Task } from "../task/Task" +import { formatResponse } from "../prompts/responses" +import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" + +// Mock search results for demonstration +// In a real implementation, this would integrate with a search API +const mockSearchResults = [ + { + title: "Getting started with Chrome Extension development", + url: "https://developer.chrome.com/docs/extensions/get-started", + snippet: + "Learn how to create your first Chrome extension with manifest v3. This guide covers the basics of extension development including manifest files, background scripts, and content scripts.", + }, + { + title: "Chrome Extension Manifest V3 Documentation", + url: "https://developer.chrome.com/docs/extensions/reference/manifest", + snippet: + "Complete reference for Chrome Extension Manifest V3. Includes all required and optional fields, permissions, and migration guide from V2.", + }, + { + title: "Web Extensions API Documentation", + url: "https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions", + snippet: + "Cross-browser extension development guide. Learn how to build extensions that work across Chrome, Firefox, and Edge browsers.", + }, +] + +export async function webSearchTool( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +) { + const query: string | undefined = block.params.query + + if (block.partial) { + return + } + + if (!query) { + cline.consecutiveMistakeCount++ + cline.recordToolError("web_search") + pushToolResult(await cline.sayAndCreateMissingParamError("web_search", "query")) + return + } + + try { + cline.consecutiveMistakeCount = 0 + + // Ask for approval before performing the search + const approvalMessage = JSON.stringify({ + tool: "webSearch", + query: removeClosingTag("query", query), + }) + + const didApprove = await askApproval("tool", approvalMessage) + + if (!didApprove) { + return + } + + // Log the search query + await cline.say("text", `Searching the web for: "${query}"`) + + // In a real implementation, this would call an actual search API + // For now, we'll return mock results to demonstrate the functionality + // This allows the tool to work without requiring additional API keys or setup + + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 500)) + + // Format the search results + let resultText = `Web search results for "${query}":\n\n` + + mockSearchResults.forEach((result, index) => { + resultText += `${index + 1}. **${result.title}**\n` + resultText += ` URL: ${result.url}\n` + resultText += ` ${result.snippet}\n\n` + }) + + resultText += `Note: This is a demonstration implementation. In production, this would integrate with a real search API like Google Custom Search, Bing Search API, or DuckDuckGo API.` + + // Record successful tool usage + cline.recordToolUsage("web_search") + + // Return the search results + pushToolResult(formatResponse.toolResult(resultText)) + } catch (error) { + await handleError("performing web search", error) + cline.recordToolError("web_search") + return + } +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 608b50752e..e0efbd92a8 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -67,6 +67,7 @@ export const toolParamNames = [ "todos", "prompt", "image", + "query", ] as const export type ToolParamName = (typeof toolParamNames)[number] @@ -176,6 +177,11 @@ export interface GenerateImageToolUse extends ToolUse { params: Partial, "prompt" | "path" | "image">> } +export interface WebSearchToolUse extends ToolUse { + name: "web_search" + params: Partial, "query">> +} + // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -204,6 +210,7 @@ export const TOOL_DISPLAY_NAMES: Record = { update_todo_list: "update todo list", run_slash_command: "run slash command", generate_image: "generate images", + web_search: "search the web", } as const // Define available tool groups. @@ -216,6 +223,7 @@ export const TOOL_GROUPS: Record = { "list_files", "list_code_definition_names", "codebase_search", + "web_search", ], }, edit: {