@iinm/plain-agent 1.1.0 → 1.2.1
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/.config/config.predefined.json +15 -0
- package/README.md +29 -21
- package/package.json +4 -8
- package/src/cliFormatter.mjs +0 -9
- package/src/config.d.ts +4 -14
- package/src/config.mjs +12 -8
- package/src/main.mjs +6 -14
- package/src/mcp.mjs +4 -8
- package/src/tools/askURL.mjs +201 -0
- package/src/tools/askWeb.mjs +200 -0
- package/src/tools/askGoogle.mjs +0 -135
- package/src/tools/fetchWebPage.mjs +0 -96
- package/src/tools/tavilySearch.d.ts +0 -6
- package/src/tools/tavilySearch.mjs +0 -57
|
@@ -701,6 +701,21 @@
|
|
|
701
701
|
}
|
|
702
702
|
}
|
|
703
703
|
},
|
|
704
|
+
{
|
|
705
|
+
"name": "glm-5",
|
|
706
|
+
"variant": "bedrock",
|
|
707
|
+
"platform": {
|
|
708
|
+
"name": "bedrock",
|
|
709
|
+
"variant": "default"
|
|
710
|
+
},
|
|
711
|
+
"model": {
|
|
712
|
+
"format": "openai-messages",
|
|
713
|
+
"config": {
|
|
714
|
+
"model": "zai.glm-5",
|
|
715
|
+
"reasoning_effort": "high"
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
},
|
|
704
719
|
{
|
|
705
720
|
"name": "glm-5",
|
|
706
721
|
"variant": "ollama",
|
package/README.md
CHANGED
|
@@ -87,19 +87,33 @@ Create the configuration.
|
|
|
87
87
|
|
|
88
88
|
// Optional
|
|
89
89
|
"tools": {
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
"askWeb": {
|
|
91
|
+
"provider": "gemini",
|
|
92
|
+
"apiKey": "FIXME",
|
|
93
|
+
"model": "gemini-3-flash-preview"
|
|
94
|
+
// Optional
|
|
95
|
+
// "baseURL": "<proxy_url>"
|
|
95
96
|
|
|
96
97
|
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
97
|
-
// "
|
|
98
|
+
// "provider": "gemini-vertex-ai",
|
|
98
99
|
// "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
|
|
100
|
+
// "model": "gemini-3-flash-preview"
|
|
101
|
+
// Optional:
|
|
102
|
+
// "account": "<service_account_email>"
|
|
99
103
|
},
|
|
100
104
|
|
|
101
|
-
"
|
|
102
|
-
"
|
|
105
|
+
"askURL": {
|
|
106
|
+
"provider": "gemini",
|
|
107
|
+
"apiKey": "FIXME"
|
|
108
|
+
// Optional
|
|
109
|
+
// "baseURL": "<proxy_url>"
|
|
110
|
+
|
|
111
|
+
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
112
|
+
// "provider": "gemini-vertex-ai",
|
|
113
|
+
// "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
|
|
114
|
+
// "model": "gemini-3-flash-preview"
|
|
115
|
+
// Optional:
|
|
116
|
+
// "account": "<service_account_email>"
|
|
103
117
|
}
|
|
104
118
|
},
|
|
105
119
|
|
|
@@ -173,9 +187,8 @@ The agent can use the following tools to assist with tasks:
|
|
|
173
187
|
- **write_file**: Write a file.
|
|
174
188
|
- **patch_file**: Patch a file.
|
|
175
189
|
- **tmux_command**: Run a tmux command.
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
178
|
-
- **search_web**: Search the web for information (requires Tavily API key).
|
|
190
|
+
- **ask_web**: Use the web search to answer questions that need up-to-date information or supporting sources. (requires Google API key or Vertex AI configuration).
|
|
191
|
+
- **ask_url**: Use one or more provided URLs to answer a question. Include the URLs in your question. (requires Google API key or Vertex AI configuration).
|
|
179
192
|
- **delegate_to_subagent**: Delegate a subtask to a subagent. The agent switches to a subagent role within the same conversation, focusing on the specified goal.
|
|
180
193
|
- **report_as_subagent**: Report completion and return to the main agent. Used by subagents to communicate results and restore the main agent role. After reporting, the subagent's conversation history is removed from the context.
|
|
181
194
|
|
|
@@ -218,12 +231,6 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
218
231
|
"defaultAction": "deny",
|
|
219
232
|
"maxApprovals": 100,
|
|
220
233
|
"patterns": [
|
|
221
|
-
// Prohibit direct access to external URLs (even GET requests can leak data via URL parameters)
|
|
222
|
-
{
|
|
223
|
-
"toolName": "fetch_web_page",
|
|
224
|
-
"action": "deny",
|
|
225
|
-
"reason": "Use ask_google instead"
|
|
226
|
-
},
|
|
227
234
|
{
|
|
228
235
|
"toolName": { "$regex": "^(write_file|patch_file)$" },
|
|
229
236
|
"action": "allow"
|
|
@@ -233,11 +240,10 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
233
240
|
"action": "allow"
|
|
234
241
|
},
|
|
235
242
|
{
|
|
236
|
-
"toolName": "
|
|
243
|
+
"toolName": { "$regex": "^(ask_web|ask_url)$" },
|
|
237
244
|
"action": "allow"
|
|
238
245
|
}
|
|
239
|
-
|
|
240
|
-
// ⚠️ Never do this. fetch_web_page and mcp run outside the sandbox, so they can send anything externally.
|
|
246
|
+
// ⚠️ Never do this. mcp run outside the sandbox, so they can send anything externally.
|
|
241
247
|
// {
|
|
242
248
|
// "toolName": { "$regex": "." },
|
|
243
249
|
// "action": "allow"
|
|
@@ -269,6 +275,7 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
269
275
|
```js
|
|
270
276
|
{
|
|
271
277
|
"autoApproval": {
|
|
278
|
+
"defaultAction": "ask",
|
|
272
279
|
// The maximum number of automatic approvals.
|
|
273
280
|
"maxApprovals": 50,
|
|
274
281
|
// Patterns are evaluated in order. First match wins.
|
|
@@ -290,8 +297,9 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
290
297
|
"input": { "command": "npm", "args": ["run", { "$regex": "^(check|test|lint|fix)$" }] },
|
|
291
298
|
"action": "allow"
|
|
292
299
|
},
|
|
300
|
+
|
|
293
301
|
{
|
|
294
|
-
"toolName": "
|
|
302
|
+
"toolName": { "$regex": "^(ask_web|ask_url)$" },
|
|
295
303
|
"action": "allow"
|
|
296
304
|
},
|
|
297
305
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iinm/plain-agent",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A lightweight CLI-based coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,21 +37,17 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@aws-crypto/sha256-js": "^5.2.0",
|
|
39
39
|
"@aws-sdk/credential-providers": "^3.1001.0",
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
40
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
41
|
+
"@modelcontextprotocol/client": "^2.0.0-alpha.2",
|
|
42
42
|
"@smithy/protocol-http": "^5.3.10",
|
|
43
43
|
"@smithy/signature-v4": "^5.3.10",
|
|
44
44
|
"diff": "^8.0.3",
|
|
45
|
-
"js-yaml": "^4.1.1"
|
|
46
|
-
"jsdom": "^28.1.0",
|
|
47
|
-
"turndown": "^7.2.2"
|
|
45
|
+
"js-yaml": "^4.1.1"
|
|
48
46
|
},
|
|
49
47
|
"devDependencies": {
|
|
50
48
|
"@biomejs/biome": "^2.4.5",
|
|
51
49
|
"@types/js-yaml": "^4.0.9",
|
|
52
|
-
"@types/jsdom": "^28.0.0",
|
|
53
50
|
"@types/node": "^22.19.13",
|
|
54
|
-
"@types/turndown": "^5.0.6",
|
|
55
51
|
"typescript": "^5.9.3"
|
|
56
52
|
}
|
|
57
53
|
}
|
package/src/cliFormatter.mjs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* @import { PatchFileInput } from "./tools/patchFile"
|
|
5
5
|
* @import { WriteFileInput } from "./tools/writeFile"
|
|
6
6
|
* @import { TmuxCommandInput } from "./tools/tmuxCommand"
|
|
7
|
-
* @import { TavilySearchInput } from "./tools/tavilySearch"
|
|
8
7
|
* @import { DelegateToSubagentInput } from "./tools/delegateToSubagent"
|
|
9
8
|
*/
|
|
10
9
|
|
|
@@ -86,14 +85,6 @@ export function formatToolUse(toolUse) {
|
|
|
86
85
|
].join("\n");
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
if (toolName === "search_web") {
|
|
90
|
-
/** @type {Partial<TavilySearchInput>} */
|
|
91
|
-
const tavilySearchInput = input;
|
|
92
|
-
return [`tool: ${toolName}`, `query: ${tavilySearchInput.query}`].join(
|
|
93
|
-
"\n",
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
88
|
if (toolName === "delegate_to_subagent") {
|
|
98
89
|
/** @type {Partial<DelegateToSubagentInput>} */
|
|
99
90
|
const delegateInput = input;
|
package/src/config.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ModelDefinition, PlatformConfig } from "./modelDefinition";
|
|
2
2
|
import { ToolUsePattern } from "./tool";
|
|
3
|
+
import { AskURLToolOptions } from "./tools/askURL.mjs";
|
|
4
|
+
import { AskWebToolOptions } from "./tools/askWeb.mjs";
|
|
3
5
|
import { ExecCommandSanboxConfig } from "./tools/execCommand";
|
|
4
6
|
|
|
5
7
|
export type ClaudeCodePluginConfig = {
|
|
@@ -18,20 +20,8 @@ export type AppConfig = {
|
|
|
18
20
|
};
|
|
19
21
|
sandbox?: ExecCommandSanboxConfig;
|
|
20
22
|
tools?: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* - AI Studio: requires apiKey
|
|
24
|
-
*/
|
|
25
|
-
askGoogle?: {
|
|
26
|
-
platform?: "vertex-ai";
|
|
27
|
-
baseURL?: string;
|
|
28
|
-
account?: string;
|
|
29
|
-
apiKey?: string;
|
|
30
|
-
model?: string;
|
|
31
|
-
};
|
|
32
|
-
searchWeb?: {
|
|
33
|
-
tavilyApiKey?: string;
|
|
34
|
-
};
|
|
23
|
+
askWeb?: AskWebToolOptions;
|
|
24
|
+
askURL?: AskURLToolOptions;
|
|
35
25
|
};
|
|
36
26
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
37
27
|
notifyCmd?: string;
|
package/src/config.mjs
CHANGED
|
@@ -76,14 +76,18 @@ export async function loadAppConfig(options = {}) {
|
|
|
76
76
|
},
|
|
77
77
|
sandbox: config.sandbox ?? merged.sandbox,
|
|
78
78
|
tools: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
askWeb: config.tools?.askWeb
|
|
80
|
+
? {
|
|
81
|
+
...(merged.tools?.askWeb ?? {}),
|
|
82
|
+
...config.tools.askWeb,
|
|
83
|
+
}
|
|
84
|
+
: merged.tools?.askWeb,
|
|
85
|
+
askURL: config.tools?.askURL
|
|
86
|
+
? {
|
|
87
|
+
...(merged.tools?.askURL ?? {}),
|
|
88
|
+
...config.tools.askURL,
|
|
89
|
+
}
|
|
90
|
+
: merged.tools?.askWeb,
|
|
87
91
|
},
|
|
88
92
|
mcpServers: {
|
|
89
93
|
...(merged.mcpServers ?? {}),
|
package/src/main.mjs
CHANGED
|
@@ -18,13 +18,12 @@ import {
|
|
|
18
18
|
import { setupMCPServer } from "./mcp.mjs";
|
|
19
19
|
import { createModelCaller } from "./modelCaller.mjs";
|
|
20
20
|
import { createPrompt } from "./prompt.mjs";
|
|
21
|
-
import {
|
|
21
|
+
import { createAskURLTool } from "./tools/askURL.mjs";
|
|
22
|
+
import { createAskWebTool } from "./tools/askWeb.mjs";
|
|
22
23
|
import { createDelegateToSubagentTool } from "./tools/delegateToSubagent.mjs";
|
|
23
24
|
import { createExecCommandTool } from "./tools/execCommand.mjs";
|
|
24
|
-
import { fetchWebPageTool } from "./tools/fetchWebPage.mjs";
|
|
25
25
|
import { patchFileTool } from "./tools/patchFile.mjs";
|
|
26
26
|
import { createReportAsSubagentTool } from "./tools/reportAsSubagent.mjs";
|
|
27
|
-
import { createTavilySearchTool } from "./tools/tavilySearch.mjs";
|
|
28
27
|
import { createTmuxCommandTool } from "./tools/tmuxCommand.mjs";
|
|
29
28
|
import { writeFileTool } from "./tools/writeFile.mjs";
|
|
30
29
|
import { createToolUseApprover } from "./toolUseApprover.mjs";
|
|
@@ -142,23 +141,16 @@ if (cliArgs.listModels) {
|
|
|
142
141
|
writeFileTool,
|
|
143
142
|
patchFileTool,
|
|
144
143
|
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
145
|
-
fetchWebPageTool,
|
|
146
144
|
createDelegateToSubagentTool(),
|
|
147
145
|
createReportAsSubagentTool(),
|
|
148
146
|
];
|
|
149
147
|
|
|
150
|
-
if (appConfig.tools?.
|
|
151
|
-
builtinTools.push(
|
|
148
|
+
if (appConfig.tools?.askWeb) {
|
|
149
|
+
builtinTools.push(createAskWebTool(appConfig.tools.askWeb));
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
if (appConfig.tools?.
|
|
155
|
-
builtinTools.push(
|
|
156
|
-
createAskGoogleTool({
|
|
157
|
-
platform: appConfig.tools.askGoogle.platform,
|
|
158
|
-
baseURL: appConfig.tools.askGoogle.baseURL,
|
|
159
|
-
apiKey: appConfig.tools.askGoogle.apiKey,
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
152
|
+
if (appConfig.tools?.askURL) {
|
|
153
|
+
builtinTools.push(createAskURLTool(appConfig.tools.askURL));
|
|
162
154
|
}
|
|
163
155
|
|
|
164
156
|
const toolUseApprover = createToolUseApprover({
|
package/src/mcp.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
* @import { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
2
|
* @import { StructuredToolResultContent, Tool, ToolImplementation } from "./tool";
|
|
5
3
|
* @import { MCPServerConfig } from "./config";
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import { mkdir, open } from "node:fs/promises";
|
|
9
7
|
import path from "node:path";
|
|
8
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
10
9
|
import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
|
|
11
10
|
import { writeTmpFile } from "./tmpfile.mjs";
|
|
12
11
|
import { noThrow } from "./utils/noThrow.mjs";
|
|
@@ -52,7 +51,7 @@ export async function setupMCPServer(serverName, serverConfig) {
|
|
|
52
51
|
/**
|
|
53
52
|
* @typedef {Object} MCPClientOptions
|
|
54
53
|
* @property {string} serverName - The name of the MCP server.
|
|
55
|
-
* @property {StdioServerParameters} params - The transport to use for the client.
|
|
54
|
+
* @property {import("@modelcontextprotocol/client").StdioServerParameters} params - The transport to use for the client.
|
|
56
55
|
*/
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -60,10 +59,7 @@ export async function setupMCPServer(serverName, serverConfig) {
|
|
|
60
59
|
* @returns {Promise<{client: Client; cleanup: () => void}>} - The MCP client and cleanup function.
|
|
61
60
|
*/
|
|
62
61
|
async function startMCPServer(options) {
|
|
63
|
-
const mcpClient = await import("@modelcontextprotocol/
|
|
64
|
-
const mcpClientStdio = await import(
|
|
65
|
-
"@modelcontextprotocol/sdk/client/stdio.js"
|
|
66
|
-
);
|
|
62
|
+
const mcpClient = await import("@modelcontextprotocol/client");
|
|
67
63
|
|
|
68
64
|
const client = new mcpClient.Client({
|
|
69
65
|
name: "undefined",
|
|
@@ -83,7 +79,7 @@ async function startMCPServer(options) {
|
|
|
83
79
|
const logPath = path.join(logDir, `mcp--${options.serverName}.stderr`);
|
|
84
80
|
const stderrLogFile = await open(logPath, "a");
|
|
85
81
|
|
|
86
|
-
const transport = new
|
|
82
|
+
const transport = new mcpClient.StdioClientTransport({
|
|
87
83
|
...restParams,
|
|
88
84
|
env: env ? { ...defaultEnv, ...env } : undefined,
|
|
89
85
|
stderr: stderrLogFile.fd,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Tool } from '../tool'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { styleText } from "node:util";
|
|
6
|
+
import { getGoogleCloudAccessToken } from "../providers/platform/googleCloud.mjs";
|
|
7
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
8
|
+
|
|
9
|
+
/** @typedef {AskURLToolGeminiOptions | AskURLToolGeminiVertexAIOptions} AskURLToolOptions */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AskURLToolGeminiOptions
|
|
13
|
+
* @property {"gemini"} provider
|
|
14
|
+
* @property {string=} baseURL
|
|
15
|
+
* @property {string} apiKey
|
|
16
|
+
* @property {string} model
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} AskURLToolGeminiVertexAIOptions
|
|
21
|
+
* @property {"gemini-vertex-ai"} provider
|
|
22
|
+
* @property {string} baseURL
|
|
23
|
+
* @property {string=} account
|
|
24
|
+
* @property {string} model
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} AskWebInput
|
|
29
|
+
* @property {string} question
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {AskURLToolOptions} config
|
|
34
|
+
* @returns {Tool}
|
|
35
|
+
*/
|
|
36
|
+
export function createAskURLTool(config) {
|
|
37
|
+
/**
|
|
38
|
+
* @param {AskWebInput} input
|
|
39
|
+
* @param {number} retryCount
|
|
40
|
+
* @returns {Promise<string | Error>}
|
|
41
|
+
*/
|
|
42
|
+
async function askURL(input, retryCount = 0) {
|
|
43
|
+
const model = config.model ?? "gemini-3-flash-preview";
|
|
44
|
+
const url =
|
|
45
|
+
config.provider === "gemini-vertex-ai"
|
|
46
|
+
? `${config.baseURL}/publishers/google/models/${config.model}:generateContent`
|
|
47
|
+
: config.baseURL
|
|
48
|
+
? `${config.baseURL}/models/${model}:generateContent`
|
|
49
|
+
: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
50
|
+
|
|
51
|
+
/** @type {Record<string,string>} */
|
|
52
|
+
const authHeader =
|
|
53
|
+
config.provider === "gemini-vertex-ai"
|
|
54
|
+
? {
|
|
55
|
+
Authorization: `Bearer ${await getGoogleCloudAccessToken(config.account)}`,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
"x-goog-api-key": config.apiKey ?? "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const data = {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
parts: [
|
|
66
|
+
{
|
|
67
|
+
text: `I need a comprehensive answer to this question. Please note that I don't have access to external URLs, so include all relevant facts, data, or explanations directly in your response. Avoid referencing links I can't open.
|
|
68
|
+
|
|
69
|
+
Question: ${input.question}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
tools: [
|
|
75
|
+
{
|
|
76
|
+
url_context: {},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
...authHeader,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(data),
|
|
88
|
+
signal: AbortSignal.timeout(120 * 1000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (response.status === 429 || response.status >= 500) {
|
|
92
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
93
|
+
console.error(
|
|
94
|
+
styleText(
|
|
95
|
+
"yellow",
|
|
96
|
+
`Google API returned ${response.status}. Retrying in ${interval} seconds...`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
100
|
+
return askURL(input, retryCount + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
return new Error(
|
|
105
|
+
`Failed to ask Web: status=${response.status}, body=${await response.text()}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = await response.json();
|
|
110
|
+
|
|
111
|
+
const candidate = body.candidates?.[0];
|
|
112
|
+
const text = candidate?.content?.parts?.[0]?.text;
|
|
113
|
+
/** @type {{segment?:{startIndex:number,endIndex:number,text:string},groundingChunkIndices?:number[]}[] | undefined} */
|
|
114
|
+
const supports = candidate?.groundingMetadata?.groundingSupports;
|
|
115
|
+
/** @type {{web?:{uri:string,title:string}}[] | undefined} */
|
|
116
|
+
const chunks = candidate?.groundingMetadata?.groundingChunks;
|
|
117
|
+
|
|
118
|
+
if (typeof text !== "string") {
|
|
119
|
+
return new Error(
|
|
120
|
+
`Unexpected response format from Google: ${JSON.stringify(body)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} source
|
|
126
|
+
* @param {number} byteIndex
|
|
127
|
+
* @param {string} insertText
|
|
128
|
+
*/
|
|
129
|
+
const insertTextAtUtf8ByteIndex = (source, byteIndex, insertText) => {
|
|
130
|
+
const sourceBuffer = Buffer.from(source, "utf8");
|
|
131
|
+
const normalizedByteIndex = Math.max(
|
|
132
|
+
0,
|
|
133
|
+
Math.min(byteIndex, sourceBuffer.length),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return Buffer.concat([
|
|
137
|
+
sourceBuffer.subarray(0, normalizedByteIndex),
|
|
138
|
+
Buffer.from(insertText, "utf8"),
|
|
139
|
+
sourceBuffer.subarray(normalizedByteIndex),
|
|
140
|
+
]).toString("utf8");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Sort by end_index desc because Gemini grounding indexes are byte offsets
|
|
144
|
+
// into the original UTF-8 text.
|
|
145
|
+
const sortedSupports = supports?.toSorted(
|
|
146
|
+
(a, b) => (b.segment?.endIndex ?? 0) - (a.segment?.endIndex ?? 0),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Insert citations using UTF-8 byte offsets.
|
|
150
|
+
let textWithCitations = text;
|
|
151
|
+
for (const support of sortedSupports ?? []) {
|
|
152
|
+
const endIndex = support.segment?.endIndex;
|
|
153
|
+
if (
|
|
154
|
+
typeof endIndex !== "number" ||
|
|
155
|
+
!support.groundingChunkIndices?.length
|
|
156
|
+
) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
textWithCitations = insertTextAtUtf8ByteIndex(
|
|
161
|
+
textWithCitations,
|
|
162
|
+
endIndex,
|
|
163
|
+
` [${support.groundingChunkIndices.map((i) => i + 1).join(", ")}] `,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const chunkString = (chunks ?? [])
|
|
168
|
+
.map(
|
|
169
|
+
(chunk, index) =>
|
|
170
|
+
`- [${index + 1} - ${chunk.web?.title}](${chunk.web?.uri})`,
|
|
171
|
+
)
|
|
172
|
+
.join("\n");
|
|
173
|
+
|
|
174
|
+
return [textWithCitations, chunkString].join("\n\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
def: {
|
|
179
|
+
name: "ask_url",
|
|
180
|
+
description:
|
|
181
|
+
"Use one or more provided URLs to answer a question. Include the URLs in your question.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
question: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description:
|
|
188
|
+
"The question to ask, including one or more URLs to use as context.",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ["question"],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {AskWebInput} input
|
|
197
|
+
* @returns {Promise<string | Error>}
|
|
198
|
+
*/
|
|
199
|
+
impl: async (input) => await noThrow(async () => askURL(input, 0)),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Tool } from '../tool'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { styleText } from "node:util";
|
|
6
|
+
import { getGoogleCloudAccessToken } from "../providers/platform/googleCloud.mjs";
|
|
7
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
8
|
+
|
|
9
|
+
/** @typedef {AskWebToolGeminiOptions | AskWebToolGeminiVertexAIOptions} AskWebToolOptions */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AskWebToolGeminiOptions
|
|
13
|
+
* @property {"gemini"} provider
|
|
14
|
+
* @property {string=} baseURL
|
|
15
|
+
* @property {string} apiKey
|
|
16
|
+
* @property {string} model
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} AskWebToolGeminiVertexAIOptions
|
|
21
|
+
* @property {"gemini-vertex-ai"} provider
|
|
22
|
+
* @property {string} baseURL
|
|
23
|
+
* @property {string=} account
|
|
24
|
+
* @property {string} model
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} AskWebInput
|
|
29
|
+
* @property {string} question
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {AskWebToolOptions} config
|
|
34
|
+
* @returns {Tool}
|
|
35
|
+
*/
|
|
36
|
+
export function createAskWebTool(config) {
|
|
37
|
+
/**
|
|
38
|
+
* @param {AskWebInput} input
|
|
39
|
+
* @param {number} retryCount
|
|
40
|
+
* @returns {Promise<string | Error>}
|
|
41
|
+
*/
|
|
42
|
+
async function askWeb(input, retryCount = 0) {
|
|
43
|
+
const model = config.model ?? "gemini-3-flash-preview";
|
|
44
|
+
const url =
|
|
45
|
+
config.provider === "gemini-vertex-ai"
|
|
46
|
+
? `${config.baseURL}/publishers/google/models/${config.model}:generateContent`
|
|
47
|
+
: config.baseURL
|
|
48
|
+
? `${config.baseURL}/models/${model}:generateContent`
|
|
49
|
+
: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
50
|
+
|
|
51
|
+
/** @type {Record<string,string>} */
|
|
52
|
+
const authHeader =
|
|
53
|
+
config.provider === "gemini-vertex-ai"
|
|
54
|
+
? {
|
|
55
|
+
Authorization: `Bearer ${await getGoogleCloudAccessToken(config.account)}`,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
"x-goog-api-key": config.apiKey ?? "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const data = {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
parts: [
|
|
66
|
+
{
|
|
67
|
+
text: `I need a comprehensive answer to this question. Please note that I don't have access to external URLs, so include all relevant facts, data, or explanations directly in your response. Avoid referencing links I can't open.
|
|
68
|
+
|
|
69
|
+
Question: ${input.question}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
tools: [
|
|
75
|
+
{
|
|
76
|
+
google_search: {},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
...authHeader,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(data),
|
|
88
|
+
signal: AbortSignal.timeout(120 * 1000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (response.status === 429 || response.status >= 500) {
|
|
92
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
93
|
+
console.error(
|
|
94
|
+
styleText(
|
|
95
|
+
"yellow",
|
|
96
|
+
`Google API returned ${response.status}. Retrying in ${interval} seconds...`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
100
|
+
return askWeb(input, retryCount + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
return new Error(
|
|
105
|
+
`Failed to ask Web: status=${response.status}, body=${await response.text()}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = await response.json();
|
|
110
|
+
|
|
111
|
+
const candidate = body.candidates?.[0];
|
|
112
|
+
const text = candidate?.content?.parts?.[0]?.text;
|
|
113
|
+
/** @type {{segment?:{startIndex:number,endIndex:number,text:string},groundingChunkIndices?:number[]}[] | undefined} */
|
|
114
|
+
const supports = candidate?.groundingMetadata?.groundingSupports;
|
|
115
|
+
/** @type {{web?:{uri:string,title:string}}[] | undefined} */
|
|
116
|
+
const chunks = candidate?.groundingMetadata?.groundingChunks;
|
|
117
|
+
|
|
118
|
+
if (typeof text !== "string") {
|
|
119
|
+
return new Error(
|
|
120
|
+
`Unexpected response format from Google: ${JSON.stringify(body)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} source
|
|
126
|
+
* @param {number} byteIndex
|
|
127
|
+
* @param {string} insertText
|
|
128
|
+
*/
|
|
129
|
+
const insertTextAtUtf8ByteIndex = (source, byteIndex, insertText) => {
|
|
130
|
+
const sourceBuffer = Buffer.from(source, "utf8");
|
|
131
|
+
const normalizedByteIndex = Math.max(
|
|
132
|
+
0,
|
|
133
|
+
Math.min(byteIndex, sourceBuffer.length),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return Buffer.concat([
|
|
137
|
+
sourceBuffer.subarray(0, normalizedByteIndex),
|
|
138
|
+
Buffer.from(insertText, "utf8"),
|
|
139
|
+
sourceBuffer.subarray(normalizedByteIndex),
|
|
140
|
+
]).toString("utf8");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Sort by end_index desc because Gemini grounding indexes are byte offsets
|
|
144
|
+
// into the original UTF-8 text.
|
|
145
|
+
const sortedSupports = supports?.toSorted(
|
|
146
|
+
(a, b) => (b.segment?.endIndex ?? 0) - (a.segment?.endIndex ?? 0),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Insert citations using UTF-8 byte offsets.
|
|
150
|
+
let textWithCitations = text;
|
|
151
|
+
for (const support of sortedSupports ?? []) {
|
|
152
|
+
const endIndex = support.segment?.endIndex;
|
|
153
|
+
if (
|
|
154
|
+
typeof endIndex !== "number" ||
|
|
155
|
+
!support.groundingChunkIndices?.length
|
|
156
|
+
) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
textWithCitations = insertTextAtUtf8ByteIndex(
|
|
161
|
+
textWithCitations,
|
|
162
|
+
endIndex,
|
|
163
|
+
` [${support.groundingChunkIndices.map((i) => i + 1).join(", ")}] `,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const chunkString = (chunks ?? [])
|
|
168
|
+
.map(
|
|
169
|
+
(chunk, index) =>
|
|
170
|
+
`- [${index + 1} - ${chunk.web?.title}](${chunk.web?.uri})`,
|
|
171
|
+
)
|
|
172
|
+
.join("\n");
|
|
173
|
+
|
|
174
|
+
return [textWithCitations, chunkString].join("\n\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
def: {
|
|
179
|
+
name: "ask_web",
|
|
180
|
+
description:
|
|
181
|
+
"Use the web search to answer questions that need up-to-date information or supporting sources.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
question: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "The question to ask",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["question"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {AskWebInput} input
|
|
196
|
+
* @returns {Promise<string | Error>}
|
|
197
|
+
*/
|
|
198
|
+
impl: async (input) => await noThrow(async () => askWeb(input, 0)),
|
|
199
|
+
};
|
|
200
|
+
}
|
package/src/tools/askGoogle.mjs
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { styleText } from "node:util";
|
|
6
|
-
import { getGoogleCloudAccessToken } from "../providers/platform/googleCloud.mjs";
|
|
7
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {Object} AskGoogleToolOptions
|
|
11
|
-
* @property {"vertex-ai"=} platform
|
|
12
|
-
* @property {string=} baseURL
|
|
13
|
-
* @property {string=} apiKey - API key for Google AI Studio
|
|
14
|
-
* @property {string=} account - The Google Cloud account to use for Vertex AI
|
|
15
|
-
* @property {string=} model
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {Object} AskGoogleInput
|
|
20
|
-
* @property {string} question
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {AskGoogleToolOptions} config
|
|
25
|
-
* @returns {Tool}
|
|
26
|
-
*/
|
|
27
|
-
export function createAskGoogleTool(config) {
|
|
28
|
-
/**
|
|
29
|
-
* @param {AskGoogleInput} input
|
|
30
|
-
* @param {number} retryCount
|
|
31
|
-
* @returns {Promise<string | Error>}
|
|
32
|
-
*/
|
|
33
|
-
async function askGoogle(input, retryCount = 0) {
|
|
34
|
-
const model = config.model ?? "gemini-3-flash-preview";
|
|
35
|
-
const url =
|
|
36
|
-
config.platform === "vertex-ai" && config.baseURL
|
|
37
|
-
? `${config.baseURL}/publishers/google/models/${model}:generateContent`
|
|
38
|
-
: config.baseURL
|
|
39
|
-
? `${config.baseURL}/models/${model}:generateContent`
|
|
40
|
-
: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
41
|
-
|
|
42
|
-
/** @type {Record<string,string>} */
|
|
43
|
-
const authHeader =
|
|
44
|
-
config.platform === "vertex-ai"
|
|
45
|
-
? {
|
|
46
|
-
Authorization: `Bearer ${await getGoogleCloudAccessToken(config.account)}`,
|
|
47
|
-
}
|
|
48
|
-
: {
|
|
49
|
-
"x-goog-api-key": config.apiKey ?? "",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const data = {
|
|
53
|
-
contents: [
|
|
54
|
-
{
|
|
55
|
-
role: "user",
|
|
56
|
-
parts: [
|
|
57
|
-
{
|
|
58
|
-
text: `I need a comprehensive answer to this question. Please note that I don't have access to external URLs, so include all relevant facts, data, or explanations directly in your response. Avoid referencing links I can't open.
|
|
59
|
-
|
|
60
|
-
Question: ${input.question}`,
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
tools: [
|
|
66
|
-
{
|
|
67
|
-
google_search: {},
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const response = await fetch(url, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
...authHeader,
|
|
76
|
-
"Content-Type": "application/json",
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify(data),
|
|
79
|
-
signal: AbortSignal.timeout(120 * 1000),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (response.status === 429 || response.status >= 500) {
|
|
83
|
-
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
84
|
-
console.error(
|
|
85
|
-
styleText(
|
|
86
|
-
"yellow",
|
|
87
|
-
`Google API returned ${response.status}. Retrying in ${interval} seconds...`,
|
|
88
|
-
),
|
|
89
|
-
);
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
91
|
-
return askGoogle(input, retryCount + 1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
return new Error(
|
|
96
|
-
`Failed to ask Google: status=${response.status}, body=${await response.text()}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const body = await response.json();
|
|
101
|
-
|
|
102
|
-
const answer = body.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
103
|
-
|
|
104
|
-
if (typeof answer !== "string") {
|
|
105
|
-
return new Error(
|
|
106
|
-
`Unexpected response format from Google: ${JSON.stringify(body)}`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return answer;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
def: {
|
|
115
|
-
name: "ask_google",
|
|
116
|
-
description: "Ask Google a question using natural language",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
question: {
|
|
121
|
-
type: "string",
|
|
122
|
-
description: "The question to ask Google",
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
required: ["question"],
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {AskGoogleInput} input
|
|
131
|
-
* @returns {Promise<string | Error>}
|
|
132
|
-
*/
|
|
133
|
-
impl: async (input) => await noThrow(async () => askGoogle(input, 0)),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { writeTmpFile } from "../tmpfile.mjs";
|
|
6
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
7
|
-
|
|
8
|
-
const MAX_CONTENT_LENGTH = 1024 * 8;
|
|
9
|
-
|
|
10
|
-
/** @type {Tool} */
|
|
11
|
-
export const fetchWebPageTool = {
|
|
12
|
-
def: {
|
|
13
|
-
name: "fetch_web_page",
|
|
14
|
-
description:
|
|
15
|
-
"Fetch and extract web page content from a given URL, returning it as Markdown.",
|
|
16
|
-
inputSchema: {
|
|
17
|
-
type: "object",
|
|
18
|
-
properties: {
|
|
19
|
-
url: {
|
|
20
|
-
type: "string",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["url"],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {Record<string, unknown>} input
|
|
29
|
-
* @returns {Record<string, unknown>}
|
|
30
|
-
*/
|
|
31
|
-
maskApprovalInput: (input) => {
|
|
32
|
-
try {
|
|
33
|
-
const url = new URL(String(input.url));
|
|
34
|
-
return { url: url.hostname };
|
|
35
|
-
} catch {
|
|
36
|
-
return input;
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {{url: string}} input
|
|
42
|
-
* @returns {Promise<string | Error>}
|
|
43
|
-
*/
|
|
44
|
-
impl: async (input) =>
|
|
45
|
-
await noThrow(async () => {
|
|
46
|
-
const { Readability } = await import("@mozilla/readability");
|
|
47
|
-
const { JSDOM } = await import("jsdom");
|
|
48
|
-
const TurndownService = (await import("turndown")).default;
|
|
49
|
-
|
|
50
|
-
const response = await fetch(input.url, {
|
|
51
|
-
signal: AbortSignal.timeout(30 * 1000),
|
|
52
|
-
});
|
|
53
|
-
const html = await response.text();
|
|
54
|
-
const dom = new JSDOM(html, { url: input.url });
|
|
55
|
-
const reader = new Readability(dom.window.document);
|
|
56
|
-
const article = reader.parse();
|
|
57
|
-
|
|
58
|
-
if (!article?.content) {
|
|
59
|
-
return "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const turndownService = new TurndownService({
|
|
63
|
-
headingStyle: "atx",
|
|
64
|
-
bulletListMarker: "-",
|
|
65
|
-
codeBlockStyle: "fenced",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const markdown = turndownService.turndown(article.content);
|
|
69
|
-
const trimmedMarkdown = markdown.trim();
|
|
70
|
-
|
|
71
|
-
if (trimmedMarkdown.length <= MAX_CONTENT_LENGTH) {
|
|
72
|
-
return trimmedMarkdown;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const filePath = await writeTmpFile(
|
|
76
|
-
trimmedMarkdown,
|
|
77
|
-
"read_web_page",
|
|
78
|
-
"md",
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const lineCount = trimmedMarkdown.split("\n").length;
|
|
82
|
-
|
|
83
|
-
return [
|
|
84
|
-
`Content is large (${trimmedMarkdown.length} characters, ${lineCount} lines) and saved to ${filePath}`,
|
|
85
|
-
"- Use rg / awk to read specific parts",
|
|
86
|
-
].join("\n");
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Playground
|
|
91
|
-
// (async () => {
|
|
92
|
-
// const input = {
|
|
93
|
-
// url: "https://devin.ai/agents101",
|
|
94
|
-
// };
|
|
95
|
-
// console.log(await fetchWebPageTool.impl(input));
|
|
96
|
-
// })();
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
3
|
-
* @import { TavilySearchInput } from './tavilySearch'
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {{tavilyApiKey?: string}} config
|
|
10
|
-
* @returns {Tool}
|
|
11
|
-
*/
|
|
12
|
-
export function createTavilySearchTool(config) {
|
|
13
|
-
return {
|
|
14
|
-
def: {
|
|
15
|
-
name: "search_web",
|
|
16
|
-
description: "Search the web for information",
|
|
17
|
-
inputSchema: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
query: {
|
|
21
|
-
type: "string",
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
required: ["query"],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {TavilySearchInput} input
|
|
30
|
-
* @returns {Promise<string | Error>}
|
|
31
|
-
*/
|
|
32
|
-
impl: async (input) =>
|
|
33
|
-
await noThrow(async () => {
|
|
34
|
-
const response = await fetch("https://api.tavily.com/search", {
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: {
|
|
37
|
-
Authorization: `Bearer ${config.tavilyApiKey}`,
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
},
|
|
40
|
-
body: JSON.stringify({
|
|
41
|
-
...input,
|
|
42
|
-
max_results: 5,
|
|
43
|
-
}),
|
|
44
|
-
signal: AbortSignal.timeout(120 * 1000),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!response.ok) {
|
|
48
|
-
return new Error(
|
|
49
|
-
`Failed to search: status=${response.status}, body=${await response.text()}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const body = await response.json();
|
|
54
|
-
return JSON.stringify(body);
|
|
55
|
-
}),
|
|
56
|
-
};
|
|
57
|
-
}
|