@iinm/plain-agent 1.9.4 → 1.10.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/README.md +25 -67
- package/package.json +1 -1
- package/src/cliCost.mjs +21 -1
- package/src/cliFormatter.mjs +19 -12
- package/src/cliInteractive.mjs +19 -8
- package/src/config.d.ts +64 -4
- package/src/config.mjs +8 -8
- package/src/main.mjs +79 -32
- package/src/tools/webFetch.mjs +442 -0
- package/src/tools/webSearch.mjs +503 -0
- package/src/utils/createSequentialExecutor.mjs +28 -0
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
package/src/tools/askURL.mjs
DELETED
|
@@ -1,209 +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
|
-
/** @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} AskURLInput
|
|
29
|
-
* @property {string} question
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {AskURLToolOptions} config
|
|
34
|
-
* @returns {Tool}
|
|
35
|
-
*/
|
|
36
|
-
export function createAskURLTool(config) {
|
|
37
|
-
/**
|
|
38
|
-
* @param {AskURLInput} 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 {AskURLInput} input
|
|
197
|
-
* @returns {Promise<string | Error>}
|
|
198
|
-
*/
|
|
199
|
-
impl: async (input) => await noThrow(async () => askURL(input, 0)),
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* @param {Record<string, unknown>} _input
|
|
203
|
-
* @returns {Record<string, unknown>}
|
|
204
|
-
*/
|
|
205
|
-
maskApprovalInput: (_input) => {
|
|
206
|
-
return {};
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
package/src/tools/askWeb.mjs
DELETED
|
@@ -1,208 +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
|
-
/** @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
|
-
/**
|
|
201
|
-
* @param {Record<string, unknown>} _input
|
|
202
|
-
* @returns {Record<string, unknown>}
|
|
203
|
-
*/
|
|
204
|
-
maskApprovalInput: (_input) => {
|
|
205
|
-
return {};
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|