fix: WebSearch API error
Browse files- bun.lock +11 -0
- package.json +1 -0
- src/tools/WebSearchTool/WebSearchTool.ts +93 -246
- src/tools/WebSearchTool/prompt.ts +4 -3
bun.lock
CHANGED
|
@@ -54,6 +54,7 @@
|
|
| 54 |
"cli-highlight": "^2.1.11",
|
| 55 |
"code-excerpt": "^4.0.0",
|
| 56 |
"diff": "^8.0.4",
|
|
|
|
| 57 |
"emoji-regex": "^10.6.0",
|
| 58 |
"env-paths": "^4.0.0",
|
| 59 |
"execa": "^9.6.1",
|
|
@@ -618,6 +619,8 @@
|
|
| 618 |
|
| 619 |
"dom-mutator": ["dom-mutator@0.6.0", "", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
|
| 620 |
|
|
|
|
|
|
|
| 621 |
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
| 622 |
|
| 623 |
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
|
@@ -742,6 +745,8 @@
|
|
| 742 |
|
| 743 |
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
| 744 |
|
|
|
|
|
|
|
| 745 |
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
| 746 |
|
| 747 |
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
|
@@ -862,6 +867,8 @@
|
|
| 862 |
|
| 863 |
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
| 864 |
|
|
|
|
|
|
|
| 865 |
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
| 866 |
|
| 867 |
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
|
@@ -966,6 +973,8 @@
|
|
| 966 |
|
| 967 |
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
| 968 |
|
|
|
|
|
|
|
| 969 |
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
| 970 |
|
| 971 |
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
@@ -1426,6 +1435,8 @@
|
|
| 1426 |
|
| 1427 |
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
| 1428 |
|
|
|
|
|
|
|
| 1429 |
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
| 1430 |
|
| 1431 |
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
|
|
|
| 54 |
"cli-highlight": "^2.1.11",
|
| 55 |
"code-excerpt": "^4.0.0",
|
| 56 |
"diff": "^8.0.4",
|
| 57 |
+
"duck-duck-scrape": "^2.2.7",
|
| 58 |
"emoji-regex": "^10.6.0",
|
| 59 |
"env-paths": "^4.0.0",
|
| 60 |
"execa": "^9.6.1",
|
|
|
|
| 619 |
|
| 620 |
"dom-mutator": ["dom-mutator@0.6.0", "", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
|
| 621 |
|
| 622 |
+
"duck-duck-scrape": ["duck-duck-scrape@2.2.7", "", { "dependencies": { "html-entities": "^2.3.3", "needle": "^3.2.0" } }, "sha512-BEcglwnfx5puJl90KQfX+Q2q5vCguqyMpZcSRPBWk8OY55qWwV93+E+7DbIkrGDW4qkqPfUvtOUdi0lXz6lEMQ=="],
|
| 623 |
+
|
| 624 |
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
| 625 |
|
| 626 |
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
|
|
|
| 745 |
|
| 746 |
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
| 747 |
|
| 748 |
+
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
|
| 749 |
+
|
| 750 |
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
| 751 |
|
| 752 |
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
|
|
|
| 867 |
|
| 868 |
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
| 869 |
|
| 870 |
+
"needle": ["needle@3.5.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" } }, "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w=="],
|
| 871 |
+
|
| 872 |
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
| 873 |
|
| 874 |
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
|
|
|
| 973 |
|
| 974 |
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
| 975 |
|
| 976 |
+
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
| 977 |
+
|
| 978 |
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
| 979 |
|
| 980 |
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
|
|
| 1435 |
|
| 1436 |
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
| 1437 |
|
| 1438 |
+
"needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
| 1439 |
+
|
| 1440 |
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
| 1441 |
|
| 1442 |
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
package.json
CHANGED
|
@@ -69,6 +69,7 @@
|
|
| 69 |
"cli-highlight": "^2.1.11",
|
| 70 |
"code-excerpt": "^4.0.0",
|
| 71 |
"diff": "^8.0.4",
|
|
|
|
| 72 |
"emoji-regex": "^10.6.0",
|
| 73 |
"env-paths": "^4.0.0",
|
| 74 |
"execa": "^9.6.1",
|
|
|
|
| 69 |
"cli-highlight": "^2.1.11",
|
| 70 |
"code-excerpt": "^4.0.0",
|
| 71 |
"diff": "^8.0.4",
|
| 72 |
+
"duck-duck-scrape": "^2.2.7",
|
| 73 |
"emoji-regex": "^10.6.0",
|
| 74 |
"env-paths": "^4.0.0",
|
| 75 |
"execa": "^9.6.1",
|
src/tools/WebSearchTool/WebSearchTool.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
| 1 |
-
import
|
| 2 |
-
BetaContentBlock,
|
| 3 |
-
BetaWebSearchTool20250305,
|
| 4 |
-
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
| 5 |
-
import { getAPIProvider } from 'src/utils/model/providers.js'
|
| 6 |
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
| 7 |
import { z } from 'zod/v4'
|
| 8 |
-
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
| 9 |
-
import { queryModelWithStreaming } from '../../services/api/claude.js'
|
| 10 |
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 11 |
import { lazySchema } from '../../utils/lazySchema.js'
|
| 12 |
import { logError } from '../../utils/log.js'
|
| 13 |
-
import {
|
| 14 |
-
import { getMainLoopModel, getSmallFastModel } from '../../utils/model/model.js'
|
| 15 |
-
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
| 16 |
-
import { asSystemPrompt } from '../../utils/systemPromptType.js'
|
| 17 |
import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
|
| 18 |
import {
|
| 19 |
getToolUseSummary,
|
|
@@ -73,123 +64,50 @@ export type { WebSearchProgress } from '../../types/tools.js'
|
|
| 73 |
|
| 74 |
import type { WebSearchProgress } from '../../types/tools.js'
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
// [
|
| 94 |
-
// - server_tool_use
|
| 95 |
-
// - web_search_tool_result
|
| 96 |
-
// - text and citation blocks intermingled
|
| 97 |
-
// ]+ (this block repeated for each search)
|
| 98 |
-
|
| 99 |
-
const results: (SearchResult | string)[] = []
|
| 100 |
-
let textAcc = ''
|
| 101 |
-
let inText = true
|
| 102 |
-
|
| 103 |
-
for (const block of result) {
|
| 104 |
-
if (block.type === 'server_tool_use') {
|
| 105 |
-
if (inText) {
|
| 106 |
-
inText = false
|
| 107 |
-
if (textAcc.trim().length > 0) {
|
| 108 |
-
results.push(textAcc.trim())
|
| 109 |
-
}
|
| 110 |
-
textAcc = ''
|
| 111 |
}
|
| 112 |
-
continue
|
| 113 |
-
}
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
logError(new Error(errorMessage))
|
| 120 |
-
results.push(errorMessage)
|
| 121 |
-
continue
|
| 122 |
}
|
| 123 |
-
// Success case - add results to our collection
|
| 124 |
-
const hits = block.content.map(r => ({ title: r.title, url: r.url }))
|
| 125 |
-
results.push({
|
| 126 |
-
tool_use_id: block.tool_use_id,
|
| 127 |
-
content: hits,
|
| 128 |
-
})
|
| 129 |
-
}
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
inText = true
|
| 136 |
-
textAcc = block.text
|
| 137 |
-
}
|
| 138 |
}
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
if (textAcc.length) {
|
| 142 |
-
results.push(textAcc.trim())
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
return {
|
| 146 |
-
query,
|
| 147 |
-
results,
|
| 148 |
-
durationSeconds,
|
| 149 |
-
}
|
| 150 |
}
|
| 151 |
|
| 152 |
-
export const WebSearchTool = buildTool({
|
| 153 |
name: WEB_SEARCH_TOOL_NAME,
|
| 154 |
-
|
| 155 |
-
maxResultSizeChars: 100_000,
|
| 156 |
-
shouldDefer: true,
|
| 157 |
-
async description(input) {
|
| 158 |
-
return `Claude wants to search the web for: ${input.query}`
|
| 159 |
-
},
|
| 160 |
-
userFacingName() {
|
| 161 |
-
return 'Web Search'
|
| 162 |
-
},
|
| 163 |
getToolUseSummary,
|
| 164 |
getActivityDescription(input) {
|
| 165 |
const summary = getToolUseSummary(input)
|
| 166 |
return summary ? `Searching for ${summary}` : 'Searching the web'
|
| 167 |
},
|
| 168 |
isEnabled() {
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
// Enable for firstParty
|
| 173 |
-
if (provider === 'firstParty') {
|
| 174 |
-
return true
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
// Enable for Vertex AI with supported models (Claude 4.0+)
|
| 178 |
-
if (provider === 'vertex') {
|
| 179 |
-
const supportsWebSearch =
|
| 180 |
-
model.includes('claude-opus-4') ||
|
| 181 |
-
model.includes('claude-sonnet-4') ||
|
| 182 |
-
model.includes('claude-haiku-4')
|
| 183 |
-
|
| 184 |
-
return supportsWebSearch
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
// Foundry only ships models that already support Web Search
|
| 188 |
-
if (provider === 'foundry') {
|
| 189 |
-
return true
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
return false
|
| 193 |
},
|
| 194 |
get inputSchema(): InputSchema {
|
| 195 |
return inputSchema()
|
|
@@ -253,150 +171,79 @@ export const WebSearchTool = buildTool({
|
|
| 253 |
},
|
| 254 |
async call(input, context, _canUseTool, _parentMessage, onProgress) {
|
| 255 |
const startTime = performance.now()
|
| 256 |
-
const { query } = input
|
| 257 |
-
const userMessage = createUserMessage({
|
| 258 |
-
content: 'Perform a web search for the query: ' + query,
|
| 259 |
-
})
|
| 260 |
-
const toolSchema = makeToolSchema(input)
|
| 261 |
-
|
| 262 |
-
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE(
|
| 263 |
-
'tengu_plum_vx3',
|
| 264 |
-
false,
|
| 265 |
-
)
|
| 266 |
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
? { type: 'disabled' as const }
|
| 275 |
-
: context.options.thinkingConfig,
|
| 276 |
-
tools: [],
|
| 277 |
-
signal: context.abortController.signal,
|
| 278 |
-
options: {
|
| 279 |
-
getToolPermissionContext: async () => appState.toolPermissionContext,
|
| 280 |
-
model: useHaiku ? getSmallFastModel() : context.options.mainLoopModel,
|
| 281 |
-
toolChoice: useHaiku ? { type: 'tool', name: 'web_search' } : undefined,
|
| 282 |
-
isNonInteractiveSession: context.options.isNonInteractiveSession,
|
| 283 |
-
hasAppendSystemPrompt: !!context.options.appendSystemPrompt,
|
| 284 |
-
extraToolSchemas: [toolSchema],
|
| 285 |
-
querySource: 'web_search_tool',
|
| 286 |
-
agents: context.options.agentDefinitions.activeAgents,
|
| 287 |
-
mcpTools: [],
|
| 288 |
-
agentId: context.agentId,
|
| 289 |
-
effortValue: appState.effortValue,
|
| 290 |
-
},
|
| 291 |
-
})
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
-
//
|
| 306 |
-
if (
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
// The actual query comes through input_json_delta events
|
| 316 |
-
continue
|
| 317 |
-
}
|
| 318 |
}
|
| 319 |
|
| 320 |
-
//
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
// Look for a complete query field
|
| 333 |
-
const queryMatch = currentToolUseJson.match(
|
| 334 |
-
/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/,
|
| 335 |
-
)
|
| 336 |
-
if (queryMatch && queryMatch[1]) {
|
| 337 |
-
// The regex properly handles escaped characters
|
| 338 |
-
const query = jsonParse('"' + queryMatch[1] + '"')
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
toolUseQueries.set(currentToolUseId, query)
|
| 345 |
-
progressCounter++
|
| 346 |
-
if (onProgress) {
|
| 347 |
-
onProgress({
|
| 348 |
-
toolUseID: `search-progress-${progressCounter}`,
|
| 349 |
-
data: {
|
| 350 |
-
type: 'query_update',
|
| 351 |
-
query,
|
| 352 |
-
},
|
| 353 |
-
})
|
| 354 |
-
}
|
| 355 |
-
}
|
| 356 |
-
}
|
| 357 |
-
} catch {
|
| 358 |
-
// Ignore parsing errors for partial JSON
|
| 359 |
-
}
|
| 360 |
-
}
|
| 361 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
) {
|
| 368 |
-
const contentBlock = event.event.content_block
|
| 369 |
-
if (contentBlock && contentBlock.type === 'web_search_tool_result') {
|
| 370 |
-
// Get the actual query that was used for this search
|
| 371 |
-
const toolUseId = contentBlock.tool_use_id
|
| 372 |
-
const actualQuery = toolUseQueries.get(toolUseId) || query
|
| 373 |
-
const content = contentBlock.content
|
| 374 |
-
|
| 375 |
-
progressCounter++
|
| 376 |
-
if (onProgress) {
|
| 377 |
-
onProgress({
|
| 378 |
-
toolUseID: toolUseId || `search-progress-${progressCounter}`,
|
| 379 |
-
data: {
|
| 380 |
-
type: 'search_results_received',
|
| 381 |
-
resultCount: Array.isArray(content) ? content.length : 0,
|
| 382 |
-
query: actualQuery,
|
| 383 |
-
},
|
| 384 |
-
})
|
| 385 |
-
}
|
| 386 |
-
}
|
| 387 |
}
|
| 388 |
}
|
| 389 |
-
|
| 390 |
-
// Process the final result
|
| 391 |
-
const endTime = performance.now()
|
| 392 |
-
const durationSeconds = (endTime - startTime) / 1000
|
| 393 |
-
|
| 394 |
-
const data = makeOutputFromSearchResponse(
|
| 395 |
-
allContentBlocks,
|
| 396 |
-
query,
|
| 397 |
-
durationSeconds,
|
| 398 |
-
)
|
| 399 |
-
return { data }
|
| 400 |
},
|
| 401 |
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
| 402 |
const { query, results } = output
|
|
@@ -432,4 +279,4 @@ export const WebSearchTool = buildTool({
|
|
| 432 |
content: formattedOutput.trim(),
|
| 433 |
}
|
| 434 |
},
|
| 435 |
-
} satisfies ToolDef<InputSchema, Output, WebSearchProgress>
|
|
|
|
| 1 |
+
import { search } from 'duck-duck-scrape'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
| 3 |
import { z } from 'zod/v4'
|
|
|
|
|
|
|
| 4 |
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 5 |
import { lazySchema } from '../../utils/lazySchema.js'
|
| 6 |
import { logError } from '../../utils/log.js'
|
| 7 |
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
|
|
|
|
|
|
|
|
|
| 8 |
import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
|
| 9 |
import {
|
| 10 |
getToolUseSummary,
|
|
|
|
| 64 |
|
| 65 |
import type { WebSearchProgress } from '../../types/tools.js'
|
| 66 |
|
| 67 |
+
/**
|
| 68 |
+
* Filter search results by domain
|
| 69 |
+
*/
|
| 70 |
+
function filterDomains(
|
| 71 |
+
results: Array<{ url: string }>,
|
| 72 |
+
allowedDomains?: string[],
|
| 73 |
+
blockedDomains?: string[]
|
| 74 |
+
): Array<{ url: string }> {
|
| 75 |
+
return results.filter(result => {
|
| 76 |
+
try {
|
| 77 |
+
const url = new URL(result.url)
|
| 78 |
+
const domain = url.hostname
|
| 79 |
+
|
| 80 |
+
if (allowedDomains?.length > 0) {
|
| 81 |
+
return allowedDomains.some(allowed =>
|
| 82 |
+
domain === allowed || domain.endsWith(`.${allowed}`)
|
| 83 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
if (blockedDomains?.length > 0) {
|
| 87 |
+
return !blockedDomains.some(blocked =>
|
| 88 |
+
domain === blocked || domain.endsWith(`.${blocked}`)
|
| 89 |
+
)
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
return true
|
| 93 |
+
} catch {
|
| 94 |
+
// Invalid URL, filter it out
|
| 95 |
+
return false
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
+
export const WebSearchTool = buildTool<InputSchema, Output, WebSearchTool>({
|
| 101 |
name: WEB_SEARCH_TOOL_NAME,
|
| 102 |
+
description: 'Search the web and return search results with titles, URLs, and snippets.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
getToolUseSummary,
|
| 104 |
getActivityDescription(input) {
|
| 105 |
const summary = getToolUseSummary(input)
|
| 106 |
return summary ? `Searching for ${summary}` : 'Searching the web'
|
| 107 |
},
|
| 108 |
isEnabled() {
|
| 109 |
+
// DuckDuckGo works with all providers, including local models
|
| 110 |
+
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
},
|
| 112 |
get inputSchema(): InputSchema {
|
| 113 |
return inputSchema()
|
|
|
|
| 171 |
},
|
| 172 |
async call(input, context, _canUseTool, _parentMessage, onProgress) {
|
| 173 |
const startTime = performance.now()
|
| 174 |
+
const { query, allowed_domains, blocked_domains } = input
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
// Progress update: starting search
|
| 177 |
+
if (onProgress) {
|
| 178 |
+
onProgress({
|
| 179 |
+
toolUseID: 'search-progress-1',
|
| 180 |
+
data: { type: 'query_update', query },
|
| 181 |
+
})
|
| 182 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
try {
|
| 185 |
+
// Call DuckDuckGo search
|
| 186 |
+
const response = await search(query, {
|
| 187 |
+
safeSearch: 'moderate',
|
| 188 |
+
})
|
| 189 |
|
| 190 |
+
// Filter results by domain if specified
|
| 191 |
+
let filteredResults = response.results
|
| 192 |
+
if (allowed_domains || blocked_domains) {
|
| 193 |
+
filteredResults = filterDomains(
|
| 194 |
+
response.results,
|
| 195 |
+
allowed_domains,
|
| 196 |
+
blocked_domains
|
| 197 |
+
)
|
| 198 |
}
|
| 199 |
|
| 200 |
+
// Progress update: results received
|
| 201 |
+
if (onProgress) {
|
| 202 |
+
onProgress({
|
| 203 |
+
toolUseID: 'search-progress-2',
|
| 204 |
+
data: {
|
| 205 |
+
type: 'search_results_received',
|
| 206 |
+
resultCount: filteredResults.length,
|
| 207 |
+
query,
|
| 208 |
+
},
|
| 209 |
+
})
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
+
// Convert to output format
|
| 213 |
+
const results: (SearchResult | string)[] = []
|
| 214 |
+
|
| 215 |
+
if (filteredResults.length === 0) {
|
| 216 |
+
results.push(`No results for: ${query}`)
|
| 217 |
+
} else {
|
| 218 |
+
results.push({
|
| 219 |
+
tool_use_id: 'search-1',
|
| 220 |
+
content: filteredResults.map(r => ({
|
| 221 |
+
title: r.title,
|
| 222 |
+
url: r.url,
|
| 223 |
+
}))
|
| 224 |
+
})
|
| 225 |
+
}
|
| 226 |
|
| 227 |
+
const endTime = performance.now()
|
| 228 |
+
const durationSeconds = (endTime - startTime) / 1000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
+
return {
|
| 231 |
+
query,
|
| 232 |
+
results,
|
| 233 |
+
durationSeconds,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
}
|
| 235 |
+
} catch (error) {
|
| 236 |
+
logError(error)
|
| 237 |
+
|
| 238 |
+
const endTime = performance.now()
|
| 239 |
+
const durationSeconds = (endTime - startTime) / 1000
|
| 240 |
|
| 241 |
+
return {
|
| 242 |
+
query,
|
| 243 |
+
results: [`Error: ${error instanceof Error ? error.message : String(error)}`],
|
| 244 |
+
durationSeconds,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
}
|
| 246 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
},
|
| 248 |
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
| 249 |
const { query, results } = output
|
|
|
|
| 279 |
content: formattedOutput.trim(),
|
| 280 |
}
|
| 281 |
},
|
| 282 |
+
}) satisfies ToolDef<InputSchema, Output, WebSearchProgress>
|
src/tools/WebSearchTool/prompt.ts
CHANGED
|
@@ -5,11 +5,11 @@ export const WEB_SEARCH_TOOL_NAME = 'WebSearch'
|
|
| 5 |
export function getWebSearchPrompt(): string {
|
| 6 |
const currentMonthYear = getLocalMonthYear()
|
| 7 |
return `
|
| 8 |
-
- Allows Claude to search the web and use the results to inform responses
|
| 9 |
- Provides up-to-date information for current events and recent data
|
| 10 |
- Returns search result information formatted as search result blocks, including links as markdown hyperlinks
|
| 11 |
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
| 12 |
-
-
|
| 13 |
|
| 14 |
CRITICAL REQUIREMENT - You MUST follow this:
|
| 15 |
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
|
|
@@ -25,7 +25,8 @@ CRITICAL REQUIREMENT - You MUST follow this:
|
|
| 25 |
|
| 26 |
Usage notes:
|
| 27 |
- Domain filtering is supported to include or block specific websites
|
| 28 |
-
-
|
|
|
|
| 29 |
|
| 30 |
IMPORTANT - Use the correct year in search queries:
|
| 31 |
- The current month is ${currentMonthYear}. You MUST use this year when searching for recent information, documentation, or current events.
|
|
|
|
| 5 |
export function getWebSearchPrompt(): string {
|
| 6 |
const currentMonthYear = getLocalMonthYear()
|
| 7 |
return `
|
| 8 |
+
- Allows Claude to search the web using DuckDuckGo and use the results to inform responses
|
| 9 |
- Provides up-to-date information for current events and recent data
|
| 10 |
- Returns search result information formatted as search result blocks, including links as markdown hyperlinks
|
| 11 |
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
| 12 |
+
- Works with all AI providers including local models
|
| 13 |
|
| 14 |
CRITICAL REQUIREMENT - You MUST follow this:
|
| 15 |
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
|
|
|
|
| 25 |
|
| 26 |
Usage notes:
|
| 27 |
- Domain filtering is supported to include or block specific websites
|
| 28 |
+
- DuckDuckGo search is available globally
|
| 29 |
+
- Rate limit: approximately 30 requests per minute
|
| 30 |
|
| 31 |
IMPORTANT - Use the correct year in search queries:
|
| 32 |
- The current month is ${currentMonthYear}. You MUST use this year when searching for recent information, documentation, or current events.
|