@oh-my-pi/anthropic-websearch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/package.json +49 -0
- package/tools/anthropic-websearch/index.ts +404 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @oh-my-pi/anthropic-websearch
|
|
2
|
+
|
|
3
|
+
Claude web search tool for [pi](https://github.com/badlogic/pi-mono).
|
|
4
|
+
|
|
5
|
+
Uses Anthropic's built-in `web_search_20250305` tool to search the web and synthesize answers with citations.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
omp install @oh-my-pi/anthropic-websearch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
The plugin checks for credentials in this order:
|
|
16
|
+
|
|
17
|
+
1. **Explicit override**: `ANTHROPIC_SEARCH_API_KEY` / `ANTHROPIC_SEARCH_BASE_URL`
|
|
18
|
+
2. **models.json**: Provider with `api: "anthropic-messages"` in `~/.pi/agent/models.json`
|
|
19
|
+
3. **OAuth**: Anthropic OAuth credentials in `~/.pi/agent/auth.json`
|
|
20
|
+
4. **Fallback**: `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`
|
|
21
|
+
|
|
22
|
+
This ordering prevents accidentally charging your console account if you have a proxy or OAuth set up.
|
|
23
|
+
|
|
24
|
+
### Example: Using a proxy
|
|
25
|
+
|
|
26
|
+
If your `~/.pi/agent/models.json` has:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"providers": {
|
|
31
|
+
"my-proxy": {
|
|
32
|
+
"baseUrl": "http://localhost:4000",
|
|
33
|
+
"apiKey": "none",
|
|
34
|
+
"api": "anthropic-messages",
|
|
35
|
+
"models": [...]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The plugin will automatically use `http://localhost:4000` with no API key.
|
|
42
|
+
|
|
43
|
+
### Example: Direct API key
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export ANTHROPIC_SEARCH_API_KEY=sk-ant-xxx
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
### `anthropic_web_search`
|
|
52
|
+
|
|
53
|
+
Search the web using Claude's built-in web search capability.
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
|
|
57
|
+
- `query` (required): The search query or question
|
|
58
|
+
- `system_prompt`: Guide the response style and focus
|
|
59
|
+
- `max_tokens`: Maximum tokens in response (default: 4096)
|
|
60
|
+
|
|
61
|
+
**Response includes:**
|
|
62
|
+
|
|
63
|
+
- Synthesized answer with inline citations
|
|
64
|
+
- List of sources with titles, URLs, and page ages
|
|
65
|
+
- Search queries Claude generated
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
| Variable | Env | Description |
|
|
70
|
+
| --------- | --------------------------- | -------------------------------------------------- |
|
|
71
|
+
| `apiKey` | `ANTHROPIC_SEARCH_API_KEY` | API key (optional if using proxy/oauth) |
|
|
72
|
+
| `baseUrl` | `ANTHROPIC_SEARCH_BASE_URL` | Base URL override |
|
|
73
|
+
| `model` | `ANTHROPIC_SEARCH_MODEL` | Model to use (default: `claude-sonnet-4-20250514`) |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oh-my-pi/anthropic-websearch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Anthropic Claude web search tool for pi",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"omp-plugin",
|
|
7
|
+
"anthropic",
|
|
8
|
+
"claude",
|
|
9
|
+
"web-search"
|
|
10
|
+
],
|
|
11
|
+
"author": "Can Bölük <me@can.ac>",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/can1357/oh-my-pi.git",
|
|
16
|
+
"directory": "plugins/anthropic-websearch"
|
|
17
|
+
},
|
|
18
|
+
"omp": {
|
|
19
|
+
"install": [
|
|
20
|
+
{
|
|
21
|
+
"src": "tools/anthropic-websearch/index.ts",
|
|
22
|
+
"dest": "agent/tools/anthropic-websearch/index.ts"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"variables": {
|
|
26
|
+
"apiKey": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"env": "ANTHROPIC_SEARCH_API_KEY",
|
|
29
|
+
"description": "Anthropic API key for web search (optional if using models.json provider or oauth)",
|
|
30
|
+
"required": false
|
|
31
|
+
},
|
|
32
|
+
"baseUrl": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"env": "ANTHROPIC_SEARCH_BASE_URL",
|
|
35
|
+
"description": "Anthropic API base URL for web search (optional)",
|
|
36
|
+
"required": false
|
|
37
|
+
},
|
|
38
|
+
"model": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"env": "ANTHROPIC_SEARCH_MODEL",
|
|
41
|
+
"description": "Model to use for web search",
|
|
42
|
+
"default": "claude-sonnet-4-20250514"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"tools"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Web Search Tool
|
|
3
|
+
*
|
|
4
|
+
* Uses Claude's built-in web_search_20250305 tool to search the web.
|
|
5
|
+
*
|
|
6
|
+
* Auth resolution order:
|
|
7
|
+
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
|
|
8
|
+
* 2. Provider with api="anthropic-messages" in ~/.pi/agent/models.json
|
|
9
|
+
* 3. OAuth credentials in ~/.pi/agent/auth.json
|
|
10
|
+
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL as final fallback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { Type, type TSchema } from "@sinclair/typebox";
|
|
17
|
+
import type {
|
|
18
|
+
CustomAgentTool,
|
|
19
|
+
CustomToolFactory,
|
|
20
|
+
ToolAPI,
|
|
21
|
+
} from "@mariozechner/pi-coding-agent";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
24
|
+
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
25
|
+
|
|
26
|
+
interface AuthConfig {
|
|
27
|
+
apiKey: string;
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ModelsJson {
|
|
32
|
+
providers?: Record<string, {
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
api?: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface AuthJson {
|
|
40
|
+
anthropic?: {
|
|
41
|
+
type: "oauth";
|
|
42
|
+
access: string;
|
|
43
|
+
expires: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a .env file and return key-value pairs
|
|
49
|
+
*/
|
|
50
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
51
|
+
const result: Record<string, string> = {};
|
|
52
|
+
if (!fs.existsSync(filePath)) return result;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
56
|
+
for (const line of content.split("\n")) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
59
|
+
|
|
60
|
+
const eqIndex = trimmed.indexOf("=");
|
|
61
|
+
if (eqIndex === -1) continue;
|
|
62
|
+
|
|
63
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
64
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
68
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
69
|
+
) {
|
|
70
|
+
value = value.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
result[key] = value;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Ignore read errors
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get env var from process.env or .env files
|
|
84
|
+
*/
|
|
85
|
+
function getEnv(key: string): string | undefined {
|
|
86
|
+
if (process.env[key]) return process.env[key];
|
|
87
|
+
|
|
88
|
+
const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
|
|
89
|
+
if (localEnv[key]) return localEnv[key];
|
|
90
|
+
|
|
91
|
+
const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
|
|
92
|
+
if (homeEnv[key]) return homeEnv[key];
|
|
93
|
+
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read JSON file safely
|
|
99
|
+
*/
|
|
100
|
+
function readJson<T>(filePath: string): T | null {
|
|
101
|
+
try {
|
|
102
|
+
if (!fs.existsSync(filePath)) return null;
|
|
103
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
return JSON.parse(content) as T;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find auth config using priority order:
|
|
112
|
+
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
113
|
+
* 2. Provider with api="anthropic-messages" in models.json
|
|
114
|
+
* 3. OAuth in auth.json
|
|
115
|
+
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
116
|
+
*/
|
|
117
|
+
function findAuthConfig(): AuthConfig | null {
|
|
118
|
+
const piAgentDir = path.join(os.homedir(), ".pi", "agent");
|
|
119
|
+
|
|
120
|
+
// 1. Explicit search-specific env vars
|
|
121
|
+
const searchApiKey = getEnv("ANTHROPIC_SEARCH_API_KEY");
|
|
122
|
+
const searchBaseUrl = getEnv("ANTHROPIC_SEARCH_BASE_URL");
|
|
123
|
+
if (searchApiKey) {
|
|
124
|
+
return {
|
|
125
|
+
apiKey: searchApiKey,
|
|
126
|
+
baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Provider with api="anthropic-messages" in models.json
|
|
131
|
+
const modelsJson = readJson<ModelsJson>(path.join(piAgentDir, "models.json"));
|
|
132
|
+
if (modelsJson?.providers) {
|
|
133
|
+
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|
|
134
|
+
if (provider.api === "anthropic-messages" && provider.apiKey && provider.apiKey !== "none") {
|
|
135
|
+
return {
|
|
136
|
+
apiKey: provider.apiKey,
|
|
137
|
+
baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Also check for providers with baseUrl but apiKey="none" (proxy)
|
|
142
|
+
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|
|
143
|
+
if (provider.api === "anthropic-messages" && provider.baseUrl) {
|
|
144
|
+
return {
|
|
145
|
+
apiKey: provider.apiKey ?? "",
|
|
146
|
+
baseUrl: provider.baseUrl,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. OAuth credentials in auth.json
|
|
153
|
+
const authJson = readJson<AuthJson>(path.join(piAgentDir, "auth.json"));
|
|
154
|
+
if (authJson?.anthropic?.type === "oauth" && authJson.anthropic.access) {
|
|
155
|
+
// Check if not expired
|
|
156
|
+
if (authJson.anthropic.expires > Date.now()) {
|
|
157
|
+
return {
|
|
158
|
+
apiKey: authJson.anthropic.access,
|
|
159
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. Generic ANTHROPIC_API_KEY fallback
|
|
165
|
+
const apiKey = getEnv("ANTHROPIC_API_KEY");
|
|
166
|
+
const baseUrl = getEnv("ANTHROPIC_BASE_URL");
|
|
167
|
+
if (apiKey) {
|
|
168
|
+
return {
|
|
169
|
+
apiKey,
|
|
170
|
+
baseUrl: baseUrl ?? DEFAULT_BASE_URL,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Response types
|
|
178
|
+
interface ServerToolUse {
|
|
179
|
+
type: "server_tool_use";
|
|
180
|
+
id: string;
|
|
181
|
+
name: "web_search";
|
|
182
|
+
input: { query: string };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface WebSearchResult {
|
|
186
|
+
type: "web_search_result";
|
|
187
|
+
title: string;
|
|
188
|
+
url: string;
|
|
189
|
+
encrypted_content: string;
|
|
190
|
+
page_age: string | null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface WebSearchToolResult {
|
|
194
|
+
type: "web_search_tool_result";
|
|
195
|
+
tool_use_id: string;
|
|
196
|
+
content: WebSearchResult[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface Citation {
|
|
200
|
+
type: "web_search_result_location";
|
|
201
|
+
url: string;
|
|
202
|
+
title: string;
|
|
203
|
+
cited_text: string;
|
|
204
|
+
encrypted_index: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface TextBlock {
|
|
208
|
+
type: "text";
|
|
209
|
+
text: string;
|
|
210
|
+
citations?: Citation[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type ContentBlock = ServerToolUse | WebSearchToolResult | TextBlock;
|
|
214
|
+
|
|
215
|
+
interface MessagesResponse {
|
|
216
|
+
id: string;
|
|
217
|
+
type: string;
|
|
218
|
+
role: string;
|
|
219
|
+
content: ContentBlock[];
|
|
220
|
+
model: string;
|
|
221
|
+
stop_reason: string;
|
|
222
|
+
usage: {
|
|
223
|
+
input_tokens: number;
|
|
224
|
+
output_tokens: number;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Call Anthropic Messages API with web search tool
|
|
230
|
+
*/
|
|
231
|
+
async function callAnthropicWebSearch(
|
|
232
|
+
auth: AuthConfig,
|
|
233
|
+
query: string,
|
|
234
|
+
model: string,
|
|
235
|
+
systemPrompt?: string,
|
|
236
|
+
maxTokens?: number,
|
|
237
|
+
): Promise<MessagesResponse> {
|
|
238
|
+
const url = `${auth.baseUrl}/v1/messages`;
|
|
239
|
+
|
|
240
|
+
const body: Record<string, unknown> = {
|
|
241
|
+
model,
|
|
242
|
+
max_tokens: maxTokens ?? 4096,
|
|
243
|
+
messages: [{ role: "user", content: query }],
|
|
244
|
+
tools: [{ type: "web_search_20250305", name: "web_search" }],
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (systemPrompt) {
|
|
248
|
+
body.system = systemPrompt;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const headers: Record<string, string> = {
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
"anthropic-version": "2023-06-01",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Handle different auth types
|
|
257
|
+
if (auth.apiKey.startsWith("sk-ant-")) {
|
|
258
|
+
headers["x-api-key"] = auth.apiKey;
|
|
259
|
+
} else if (auth.apiKey && auth.apiKey !== "none") {
|
|
260
|
+
// OAuth token or other bearer token
|
|
261
|
+
headers["Authorization"] = `Bearer ${auth.apiKey}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
const errorText = await response.text();
|
|
272
|
+
throw new Error(`Anthropic API error (${response.status}): ${errorText}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return response.json() as Promise<MessagesResponse>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Format response for display
|
|
280
|
+
*/
|
|
281
|
+
function formatResponse(response: MessagesResponse): { text: string; details: unknown } {
|
|
282
|
+
const parts: string[] = [];
|
|
283
|
+
const searchQueries: string[] = [];
|
|
284
|
+
const sources: Array<{ title: string; url: string; age: string | null }> = [];
|
|
285
|
+
const citations: Citation[] = [];
|
|
286
|
+
|
|
287
|
+
for (const block of response.content) {
|
|
288
|
+
if (block.type === "server_tool_use" && block.name === "web_search") {
|
|
289
|
+
searchQueries.push(block.input.query);
|
|
290
|
+
} else if (block.type === "web_search_tool_result") {
|
|
291
|
+
for (const result of block.content) {
|
|
292
|
+
if (result.type === "web_search_result") {
|
|
293
|
+
sources.push({
|
|
294
|
+
title: result.title,
|
|
295
|
+
url: result.url,
|
|
296
|
+
age: result.page_age,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else if (block.type === "text") {
|
|
301
|
+
parts.push(block.text);
|
|
302
|
+
if (block.citations) {
|
|
303
|
+
citations.push(...block.citations);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let text = parts.join("\n\n");
|
|
309
|
+
|
|
310
|
+
// Add sources
|
|
311
|
+
if (sources.length > 0) {
|
|
312
|
+
text += "\n\n## Sources";
|
|
313
|
+
for (const [i, src] of sources.entries()) {
|
|
314
|
+
const age = src.age ? ` (${src.age})` : "";
|
|
315
|
+
text += `\n[${i + 1}] ${src.title}${age}\n ${src.url}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
text,
|
|
321
|
+
details: {
|
|
322
|
+
model: response.model,
|
|
323
|
+
usage: response.usage,
|
|
324
|
+
searchQueries,
|
|
325
|
+
sources,
|
|
326
|
+
citations: citations.map((c) => ({
|
|
327
|
+
url: c.url,
|
|
328
|
+
title: c.title,
|
|
329
|
+
citedText: c.cited_text,
|
|
330
|
+
})),
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Tool schema
|
|
336
|
+
const SearchSchema = Type.Object({
|
|
337
|
+
query: Type.String({
|
|
338
|
+
description: "The search query or question to answer using web search",
|
|
339
|
+
}),
|
|
340
|
+
system_prompt: Type.Optional(
|
|
341
|
+
Type.String({
|
|
342
|
+
description: "System prompt to guide the response style and focus",
|
|
343
|
+
}),
|
|
344
|
+
),
|
|
345
|
+
max_tokens: Type.Optional(
|
|
346
|
+
Type.Number({
|
|
347
|
+
description: "Maximum tokens in response (default: 4096)",
|
|
348
|
+
minimum: 1,
|
|
349
|
+
maximum: 16384,
|
|
350
|
+
}),
|
|
351
|
+
),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
type SearchParams = {
|
|
355
|
+
query: string;
|
|
356
|
+
system_prompt?: string;
|
|
357
|
+
max_tokens?: number;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const factory: CustomToolFactory = async (
|
|
361
|
+
_toolApi: ToolAPI,
|
|
362
|
+
): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
363
|
+
const auth = findAuthConfig();
|
|
364
|
+
if (!auth) {
|
|
365
|
+
console.error("anthropic-websearch: No auth config found. Set ANTHROPIC_SEARCH_API_KEY or configure models.json/auth.json");
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const model = getEnv("ANTHROPIC_SEARCH_MODEL") ?? DEFAULT_MODEL;
|
|
370
|
+
|
|
371
|
+
const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
|
|
372
|
+
name: "anthropic_web_search",
|
|
373
|
+
label: "Anthropic Web Search",
|
|
374
|
+
description: `Web search powered by Claude (${model}). Uses Claude's built-in web search capability to find current information and synthesize answers with citations. Best for questions requiring up-to-date information from the web.`,
|
|
375
|
+
parameters: SearchSchema,
|
|
376
|
+
async execute(_toolCallId, params) {
|
|
377
|
+
try {
|
|
378
|
+
const p = (params ?? {}) as SearchParams;
|
|
379
|
+
const response = await callAnthropicWebSearch(
|
|
380
|
+
auth,
|
|
381
|
+
p.query,
|
|
382
|
+
model,
|
|
383
|
+
p.system_prompt,
|
|
384
|
+
p.max_tokens,
|
|
385
|
+
);
|
|
386
|
+
const { text, details } = formatResponse(response);
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: "text" as const, text }],
|
|
389
|
+
details,
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
+
return {
|
|
394
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
395
|
+
details: { error: message },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return [tool];
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export default factory;
|