ak-gemini 1.2.0 → 2.0.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 +259 -294
- package/base.js +485 -0
- package/chat.js +87 -0
- package/code-agent.js +687 -0
- package/index.cjs +1928 -1213
- package/index.js +40 -1501
- package/json-helpers.js +352 -0
- package/message.js +170 -0
- package/package.json +14 -7
- package/tool-agent.js +312 -0
- package/transformer.js +502 -0
- package/types.d.ts +452 -241
- package/agent.js +0 -481
- package/tools.js +0 -134
package/index.cjs
CHANGED
|
@@ -29,19 +29,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
// index.js
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
-
|
|
32
|
+
BaseGemini: () => base_default,
|
|
33
|
+
Chat: () => chat_default,
|
|
34
|
+
CodeAgent: () => code_agent_default,
|
|
33
35
|
HarmBlockThreshold: () => import_genai2.HarmBlockThreshold,
|
|
34
36
|
HarmCategory: () => import_genai2.HarmCategory,
|
|
37
|
+
Message: () => message_default,
|
|
38
|
+
RagAgent: () => rag_agent_default,
|
|
35
39
|
ThinkingLevel: () => import_genai2.ThinkingLevel,
|
|
40
|
+
ToolAgent: () => tool_agent_default,
|
|
41
|
+
Transformer: () => transformer_default,
|
|
36
42
|
attemptJSONRecovery: () => attemptJSONRecovery,
|
|
37
43
|
default: () => index_default,
|
|
44
|
+
extractJSON: () => extractJSON,
|
|
38
45
|
log: () => logger_default
|
|
39
46
|
});
|
|
40
47
|
module.exports = __toCommonJS(index_exports);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
var
|
|
44
|
-
var
|
|
48
|
+
|
|
49
|
+
// base.js
|
|
50
|
+
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
51
|
+
var import_genai = require("@google/genai");
|
|
45
52
|
|
|
46
53
|
// logger.js
|
|
47
54
|
var import_pino = __toESM(require("pino"), 1);
|
|
@@ -60,117 +67,249 @@ var logger = (0, import_pino.default)({
|
|
|
60
67
|
});
|
|
61
68
|
var logger_default = logger;
|
|
62
69
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
var import_genai = require("@google/genai");
|
|
66
|
-
|
|
67
|
-
// tools.js
|
|
68
|
-
var MAX_RESPONSE_LENGTH = 5e4;
|
|
69
|
-
function parseBody(text) {
|
|
70
|
-
const body = text.length > MAX_RESPONSE_LENGTH ? text.slice(0, MAX_RESPONSE_LENGTH) + "\n...[TRUNCATED]" : text;
|
|
70
|
+
// json-helpers.js
|
|
71
|
+
function isJSON(data) {
|
|
71
72
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const attempt = JSON.stringify(data);
|
|
74
|
+
if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
|
|
75
|
+
if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return false;
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
filename: { type: "string", description: 'Suggested filename for the document (e.g. "report.md")' },
|
|
118
|
-
title: { type: "string", description: "Document title" },
|
|
119
|
-
content: { type: "string", description: "Full markdown content of the document" }
|
|
120
|
-
},
|
|
121
|
-
required: ["filename", "content"]
|
|
84
|
+
function isJSONStr(string) {
|
|
85
|
+
if (typeof string !== "string") return false;
|
|
86
|
+
try {
|
|
87
|
+
const result = JSON.parse(string);
|
|
88
|
+
const type = Object.prototype.toString.call(result);
|
|
89
|
+
return type === "[object Object]" || type === "[object Array]";
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function attemptJSONRecovery(text, maxAttempts = 100) {
|
|
95
|
+
if (!text || typeof text !== "string") return null;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
}
|
|
100
|
+
let workingText = text.trim();
|
|
101
|
+
let braces = 0;
|
|
102
|
+
let brackets = 0;
|
|
103
|
+
let inString = false;
|
|
104
|
+
let escapeNext = false;
|
|
105
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
106
|
+
const char = workingText[j];
|
|
107
|
+
if (escapeNext) {
|
|
108
|
+
escapeNext = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (char === "\\") {
|
|
112
|
+
escapeNext = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === '"') {
|
|
116
|
+
inString = !inString;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!inString) {
|
|
120
|
+
if (char === "{") braces++;
|
|
121
|
+
else if (char === "}") braces--;
|
|
122
|
+
else if (char === "[") brackets++;
|
|
123
|
+
else if (char === "]") brackets--;
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
|
|
127
|
+
let fixedText = workingText;
|
|
128
|
+
if (inString) {
|
|
129
|
+
fixedText += '"';
|
|
130
|
+
}
|
|
131
|
+
while (braces > 0) {
|
|
132
|
+
fixedText += "}";
|
|
133
|
+
braces--;
|
|
134
|
+
}
|
|
135
|
+
while (brackets > 0) {
|
|
136
|
+
fixedText += "]";
|
|
137
|
+
brackets--;
|
|
138
|
+
}
|
|
128
139
|
try {
|
|
129
|
-
|
|
140
|
+
const result = JSON.parse(fixedText);
|
|
141
|
+
if (logger_default.level !== "silent") {
|
|
142
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
130
145
|
} catch (e) {
|
|
131
|
-
logger_default.warn(`onToolCall callback error: ${e.message}`);
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
|
|
149
|
+
workingText = workingText.slice(0, -1);
|
|
150
|
+
let braces2 = 0;
|
|
151
|
+
let brackets2 = 0;
|
|
152
|
+
let inString2 = false;
|
|
153
|
+
let escapeNext2 = false;
|
|
154
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
155
|
+
const char = workingText[j];
|
|
156
|
+
if (escapeNext2) {
|
|
157
|
+
escapeNext2 = false;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (char === "\\") {
|
|
161
|
+
escapeNext2 = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (char === '"') {
|
|
165
|
+
inString2 = !inString2;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!inString2) {
|
|
169
|
+
if (char === "{") braces2++;
|
|
170
|
+
else if (char === "}") braces2--;
|
|
171
|
+
else if (char === "[") brackets2++;
|
|
172
|
+
else if (char === "]") brackets2--;
|
|
173
|
+
}
|
|
156
174
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
175
|
+
if (braces2 === 0 && brackets2 === 0 && !inString2) {
|
|
176
|
+
try {
|
|
177
|
+
const result = JSON.parse(workingText);
|
|
178
|
+
if (logger_default.level !== "silent") {
|
|
179
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (i > 5) {
|
|
186
|
+
let fixedText = workingText;
|
|
187
|
+
if (inString2) {
|
|
188
|
+
fixedText += '"';
|
|
189
|
+
}
|
|
190
|
+
while (braces2 > 0) {
|
|
191
|
+
fixedText += "}";
|
|
192
|
+
braces2--;
|
|
193
|
+
}
|
|
194
|
+
while (brackets2 > 0) {
|
|
195
|
+
fixedText += "]";
|
|
196
|
+
brackets2--;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const result = JSON.parse(fixedText);
|
|
200
|
+
if (logger_default.level !== "silent") {
|
|
201
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function extractCompleteStructure(text, startPos) {
|
|
211
|
+
const startChar = text[startPos];
|
|
212
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
213
|
+
let depth = 0;
|
|
214
|
+
let inString = false;
|
|
215
|
+
let escaped = false;
|
|
216
|
+
for (let i = startPos; i < text.length; i++) {
|
|
217
|
+
const char = text[i];
|
|
218
|
+
if (escaped) {
|
|
219
|
+
escaped = false;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (char === "\\" && inString) {
|
|
223
|
+
escaped = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === '"' && !escaped) {
|
|
227
|
+
inString = !inString;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!inString) {
|
|
231
|
+
if (char === startChar) {
|
|
232
|
+
depth++;
|
|
233
|
+
} else if (char === endChar) {
|
|
234
|
+
depth--;
|
|
235
|
+
if (depth === 0) {
|
|
236
|
+
return text.substring(startPos, i + 1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function findCompleteJSONStructures(text) {
|
|
244
|
+
const results = [];
|
|
245
|
+
const startChars = ["{", "["];
|
|
246
|
+
for (let i = 0; i < text.length; i++) {
|
|
247
|
+
if (startChars.includes(text[i])) {
|
|
248
|
+
const extracted = extractCompleteStructure(text, i);
|
|
249
|
+
if (extracted) {
|
|
250
|
+
results.push(extracted);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
function extractJSON(text) {
|
|
257
|
+
if (!text || typeof text !== "string") {
|
|
258
|
+
throw new Error("No text provided for JSON extraction");
|
|
259
|
+
}
|
|
260
|
+
if (isJSONStr(text.trim())) {
|
|
261
|
+
return JSON.parse(text.trim());
|
|
262
|
+
}
|
|
263
|
+
const codeBlockPatterns = [
|
|
264
|
+
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
265
|
+
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
266
|
+
];
|
|
267
|
+
for (const pattern of codeBlockPatterns) {
|
|
268
|
+
const matches = text.match(pattern);
|
|
269
|
+
if (matches) {
|
|
270
|
+
for (const match of matches) {
|
|
271
|
+
const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
|
|
272
|
+
if (isJSONStr(jsonContent)) {
|
|
273
|
+
return JSON.parse(jsonContent);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const jsonPatterns = [
|
|
279
|
+
/\{[\s\S]*\}/g,
|
|
280
|
+
/\[[\s\S]*\]/g
|
|
281
|
+
];
|
|
282
|
+
for (const pattern of jsonPatterns) {
|
|
283
|
+
const matches = text.match(pattern);
|
|
284
|
+
if (matches) {
|
|
285
|
+
for (const match of matches) {
|
|
286
|
+
const candidate = match.trim();
|
|
287
|
+
if (isJSONStr(candidate)) {
|
|
288
|
+
return JSON.parse(candidate);
|
|
164
289
|
}
|
|
165
290
|
}
|
|
166
|
-
return { written: true, filename: args.filename, length: args.content.length };
|
|
167
291
|
}
|
|
168
|
-
default:
|
|
169
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
170
292
|
}
|
|
293
|
+
const advancedExtract = findCompleteJSONStructures(text);
|
|
294
|
+
if (advancedExtract.length > 0) {
|
|
295
|
+
for (const candidate of advancedExtract) {
|
|
296
|
+
if (isJSONStr(candidate)) {
|
|
297
|
+
return JSON.parse(candidate);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const cleanedText = text.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, "").replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, "").replace(/^\s*The\s+.*?is\s*[:\n]/gi, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
|
|
302
|
+
if (isJSONStr(cleanedText)) {
|
|
303
|
+
return JSON.parse(cleanedText);
|
|
304
|
+
}
|
|
305
|
+
const recoveredJSON = attemptJSONRecovery(text);
|
|
306
|
+
if (recoveredJSON !== null) {
|
|
307
|
+
return recoveredJSON;
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
171
310
|
}
|
|
172
311
|
|
|
173
|
-
//
|
|
312
|
+
// base.js
|
|
174
313
|
import_dotenv.default.config();
|
|
175
314
|
var { NODE_ENV = "unknown", LOG_LEVEL = "" } = process.env;
|
|
176
315
|
var DEFAULT_SAFETY_SETTINGS = [
|
|
@@ -180,6 +319,7 @@ var DEFAULT_SAFETY_SETTINGS = [
|
|
|
180
319
|
var DEFAULT_THINKING_CONFIG = {
|
|
181
320
|
thinkingBudget: 0
|
|
182
321
|
};
|
|
322
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 5e4;
|
|
183
323
|
var THINKING_SUPPORTED_MODELS = [
|
|
184
324
|
/^gemini-3-flash(-preview)?$/,
|
|
185
325
|
/^gemini-3-pro(-preview|-image-preview)?$/,
|
|
@@ -188,20 +328,26 @@ var THINKING_SUPPORTED_MODELS = [
|
|
|
188
328
|
/^gemini-2\.5-flash-lite(-preview)?$/,
|
|
189
329
|
/^gemini-2\.0-flash$/
|
|
190
330
|
];
|
|
191
|
-
var
|
|
331
|
+
var MODEL_PRICING = {
|
|
332
|
+
"gemini-2.5-flash": { input: 0.15, output: 0.6 },
|
|
333
|
+
"gemini-2.5-flash-lite": { input: 0.02, output: 0.1 },
|
|
334
|
+
"gemini-2.5-pro": { input: 2.5, output: 10 },
|
|
335
|
+
"gemini-3-pro": { input: 2, output: 12 },
|
|
336
|
+
"gemini-3-pro-preview": { input: 2, output: 12 },
|
|
337
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
338
|
+
"gemini-2.0-flash-lite": { input: 0.02, output: 0.1 }
|
|
339
|
+
};
|
|
340
|
+
var BaseGemini = class {
|
|
192
341
|
/**
|
|
193
|
-
*
|
|
194
|
-
* @param {AIAgentOptions} [options={}] - Configuration options (see AIAgentOptions in types.d.ts)
|
|
342
|
+
* @param {BaseGeminiOptions} [options={}]
|
|
195
343
|
*/
|
|
196
344
|
constructor(options = {}) {
|
|
197
345
|
this.modelName = options.modelName || "gemini-2.5-flash";
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.onMarkdown = options.onMarkdown || null;
|
|
204
|
-
this.labels = options.labels || {};
|
|
346
|
+
if (options.systemPrompt !== void 0) {
|
|
347
|
+
this.systemPrompt = options.systemPrompt;
|
|
348
|
+
} else {
|
|
349
|
+
this.systemPrompt = null;
|
|
350
|
+
}
|
|
205
351
|
this.vertexai = options.vertexai || false;
|
|
206
352
|
this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
|
|
207
353
|
this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || void 0;
|
|
@@ -214,33 +360,34 @@ var AIAgent = class {
|
|
|
214
360
|
throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
|
|
215
361
|
}
|
|
216
362
|
this._configureLogLevel(options.logLevel);
|
|
363
|
+
this.labels = options.labels || {};
|
|
217
364
|
this.chatConfig = {
|
|
218
365
|
temperature: 0.7,
|
|
219
366
|
topP: 0.95,
|
|
220
367
|
topK: 64,
|
|
221
368
|
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
|
222
|
-
systemInstruction: this.systemPrompt,
|
|
223
|
-
maxOutputTokens: options.chatConfig?.maxOutputTokens || 5e4,
|
|
224
369
|
...options.chatConfig
|
|
225
370
|
};
|
|
226
|
-
|
|
371
|
+
if (this.systemPrompt) {
|
|
372
|
+
this.chatConfig.systemInstruction = this.systemPrompt;
|
|
373
|
+
} else if (this.systemPrompt === null && options.systemPrompt === void 0) {
|
|
374
|
+
} else if (options.systemPrompt === null || options.systemPrompt === false) {
|
|
375
|
+
delete this.chatConfig.systemInstruction;
|
|
376
|
+
}
|
|
377
|
+
if (options.maxOutputTokens !== void 0) {
|
|
378
|
+
if (options.maxOutputTokens === null) {
|
|
379
|
+
delete this.chatConfig.maxOutputTokens;
|
|
380
|
+
} else {
|
|
381
|
+
this.chatConfig.maxOutputTokens = options.maxOutputTokens;
|
|
382
|
+
}
|
|
383
|
+
} else if (options.chatConfig?.maxOutputTokens !== void 0) {
|
|
384
|
+
if (options.chatConfig.maxOutputTokens === null) {
|
|
385
|
+
delete this.chatConfig.maxOutputTokens;
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
389
|
+
}
|
|
227
390
|
this._configureThinking(options.thinkingConfig);
|
|
228
|
-
this.chatConfig.tools = [{ functionDeclarations: BUILT_IN_DECLARATIONS }];
|
|
229
|
-
this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
230
|
-
this.genAIClient = null;
|
|
231
|
-
this.chatSession = null;
|
|
232
|
-
this.lastResponseMetadata = null;
|
|
233
|
-
this._markdownFiles = [];
|
|
234
|
-
logger_default.debug(`AIAgent created with model: ${this.modelName}`);
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Initialize the agent — creates the GenAI client and chat session.
|
|
238
|
-
* Called automatically by chat() and stream() if not called explicitly.
|
|
239
|
-
* Idempotent — safe to call multiple times.
|
|
240
|
-
* @returns {Promise<void>}
|
|
241
|
-
*/
|
|
242
|
-
async init() {
|
|
243
|
-
if (this.chatSession) return;
|
|
244
391
|
const clientOptions = this.vertexai ? {
|
|
245
392
|
vertexai: true,
|
|
246
393
|
project: this.project,
|
|
@@ -248,235 +395,165 @@ var AIAgent = class {
|
|
|
248
395
|
...this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions }
|
|
249
396
|
} : { apiKey: this.apiKey };
|
|
250
397
|
this.genAIClient = new import_genai.GoogleGenAI(clientOptions);
|
|
251
|
-
this.chatSession =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
await this.genAIClient.models.list();
|
|
261
|
-
logger_default.debug("AIAgent: Gemini API connection successful.");
|
|
262
|
-
} catch (e) {
|
|
263
|
-
throw new Error(`AIAgent initialization failed: ${e.message}`);
|
|
264
|
-
}
|
|
265
|
-
logger_default.debug("AIAgent: Chat session initialized.");
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Send a message and get a complete response (non-streaming).
|
|
269
|
-
* Automatically handles the tool-use loop — if the model requests tool calls,
|
|
270
|
-
* they are executed and results sent back until the model produces a final response.
|
|
271
|
-
*
|
|
272
|
-
* @param {string} message - The user's message
|
|
273
|
-
* @returns {Promise<AgentResponse>} Response with text, toolCalls, markdownFiles, and usage
|
|
274
|
-
* @example
|
|
275
|
-
* const res = await agent.chat('Fetch https://api.example.com/users');
|
|
276
|
-
* console.log(res.text); // Agent's summary
|
|
277
|
-
* console.log(res.toolCalls); // [{name: 'http_get', args: {...}, result: {...}}]
|
|
278
|
-
*/
|
|
279
|
-
async chat(message) {
|
|
280
|
-
if (!this.chatSession) await this.init();
|
|
281
|
-
this._markdownFiles = [];
|
|
282
|
-
const allToolCalls = [];
|
|
283
|
-
let response = await this.chatSession.sendMessage({ message });
|
|
284
|
-
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
285
|
-
const functionCalls = response.functionCalls;
|
|
286
|
-
if (!functionCalls || functionCalls.length === 0) break;
|
|
287
|
-
const toolResults = await Promise.all(
|
|
288
|
-
functionCalls.map(async (call) => {
|
|
289
|
-
let result;
|
|
290
|
-
try {
|
|
291
|
-
result = await executeBuiltInTool(call.name, call.args, {
|
|
292
|
-
httpTimeout: this.httpTimeout,
|
|
293
|
-
onToolCall: this.onToolCall,
|
|
294
|
-
onMarkdown: this.onMarkdown
|
|
295
|
-
});
|
|
296
|
-
} catch (err) {
|
|
297
|
-
logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
298
|
-
result = { error: err.message };
|
|
299
|
-
}
|
|
300
|
-
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
301
|
-
if (call.name === "write_markdown" && call.args) {
|
|
302
|
-
this._markdownFiles.push({
|
|
303
|
-
filename: (
|
|
304
|
-
/** @type {string} */
|
|
305
|
-
call.args.filename
|
|
306
|
-
),
|
|
307
|
-
content: (
|
|
308
|
-
/** @type {string} */
|
|
309
|
-
call.args.content
|
|
310
|
-
)
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
return { id: call.id, name: call.name, result };
|
|
314
|
-
})
|
|
315
|
-
);
|
|
316
|
-
response = await this.chatSession.sendMessage({
|
|
317
|
-
message: toolResults.map((r) => ({
|
|
318
|
-
functionResponse: {
|
|
319
|
-
id: r.id,
|
|
320
|
-
name: r.name,
|
|
321
|
-
response: { output: r.result }
|
|
322
|
-
}
|
|
323
|
-
}))
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
this._captureMetadata(response);
|
|
327
|
-
return {
|
|
328
|
-
text: response.text || "",
|
|
329
|
-
toolCalls: allToolCalls,
|
|
330
|
-
markdownFiles: [...this._markdownFiles],
|
|
331
|
-
usage: this.getLastUsage()
|
|
398
|
+
this.chatSession = null;
|
|
399
|
+
this.lastResponseMetadata = null;
|
|
400
|
+
this.exampleCount = 0;
|
|
401
|
+
this._cumulativeUsage = {
|
|
402
|
+
promptTokens: 0,
|
|
403
|
+
responseTokens: 0,
|
|
404
|
+
totalTokens: 0,
|
|
405
|
+
attempts: 0
|
|
332
406
|
};
|
|
407
|
+
logger_default.debug(`${this.constructor.name} created with model: ${this.modelName}`);
|
|
333
408
|
}
|
|
409
|
+
// ── Initialization ───────────────────────────────────────────────────────
|
|
334
410
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* - `text` — A chunk of the agent's text response (yield as it arrives)
|
|
340
|
-
* - `tool_call` — The agent is about to call a tool (includes toolName and args)
|
|
341
|
-
* - `tool_result` — A tool finished executing (includes toolName and result)
|
|
342
|
-
* - `markdown` — A markdown document was generated (includes filename and content)
|
|
343
|
-
* - `done` — The agent finished (includes fullText, markdownFiles, usage)
|
|
344
|
-
*
|
|
345
|
-
* @param {string} message - The user's message
|
|
346
|
-
* @yields {AgentStreamEvent}
|
|
347
|
-
* @example
|
|
348
|
-
* for await (const event of agent.stream('Analyze this API...')) {
|
|
349
|
-
* if (event.type === 'text') process.stdout.write(event.text);
|
|
350
|
-
* if (event.type === 'tool_call') console.log(`Calling: ${event.toolName}`);
|
|
351
|
-
* if (event.type === 'done') console.log(`\nTokens: ${event.usage?.totalTokens}`);
|
|
352
|
-
* }
|
|
411
|
+
* Initializes the chat session. Idempotent unless force=true.
|
|
412
|
+
* Subclasses can override `_getChatCreateOptions()` to customize.
|
|
413
|
+
* @param {boolean} [force=false]
|
|
414
|
+
* @returns {Promise<void>}
|
|
353
415
|
*/
|
|
354
|
-
async
|
|
355
|
-
if (
|
|
356
|
-
this.
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (chunk.functionCalls) {
|
|
365
|
-
functionCalls.push(...chunk.functionCalls);
|
|
366
|
-
} else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
367
|
-
const text = chunk.candidates[0].content.parts[0].text;
|
|
368
|
-
roundText += text;
|
|
369
|
-
fullText += text;
|
|
370
|
-
yield { type: "text", text };
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (functionCalls.length === 0) {
|
|
374
|
-
yield {
|
|
375
|
-
type: "done",
|
|
376
|
-
fullText,
|
|
377
|
-
markdownFiles: [...this._markdownFiles],
|
|
378
|
-
usage: this.getLastUsage()
|
|
379
|
-
};
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
const toolResults = [];
|
|
383
|
-
for (const call of functionCalls) {
|
|
384
|
-
yield { type: "tool_call", toolName: call.name, args: call.args };
|
|
385
|
-
let result;
|
|
386
|
-
try {
|
|
387
|
-
result = await executeBuiltInTool(call.name, call.args, {
|
|
388
|
-
httpTimeout: this.httpTimeout,
|
|
389
|
-
onToolCall: this.onToolCall,
|
|
390
|
-
onMarkdown: this.onMarkdown
|
|
391
|
-
});
|
|
392
|
-
} catch (err) {
|
|
393
|
-
logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
394
|
-
result = { error: err.message };
|
|
395
|
-
}
|
|
396
|
-
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
397
|
-
yield { type: "tool_result", toolName: call.name, result };
|
|
398
|
-
if (call.name === "write_markdown" && call.args) {
|
|
399
|
-
const mdFilename = (
|
|
400
|
-
/** @type {string} */
|
|
401
|
-
call.args.filename
|
|
402
|
-
);
|
|
403
|
-
const mdContent = (
|
|
404
|
-
/** @type {string} */
|
|
405
|
-
call.args.content
|
|
406
|
-
);
|
|
407
|
-
this._markdownFiles.push({ filename: mdFilename, content: mdContent });
|
|
408
|
-
yield { type: "markdown", filename: mdFilename, content: mdContent };
|
|
409
|
-
}
|
|
410
|
-
toolResults.push({ id: call.id, name: call.name, result });
|
|
411
|
-
}
|
|
412
|
-
streamResponse = await this.chatSession.sendMessageStream({
|
|
413
|
-
message: toolResults.map((r) => ({
|
|
414
|
-
functionResponse: {
|
|
415
|
-
id: r.id,
|
|
416
|
-
name: r.name,
|
|
417
|
-
response: { output: r.result }
|
|
418
|
-
}
|
|
419
|
-
}))
|
|
420
|
-
});
|
|
416
|
+
async init(force = false) {
|
|
417
|
+
if (this.chatSession && !force) return;
|
|
418
|
+
logger_default.debug(`Initializing ${this.constructor.name} chat session with model: ${this.modelName}...`);
|
|
419
|
+
const chatOptions = this._getChatCreateOptions();
|
|
420
|
+
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
421
|
+
try {
|
|
422
|
+
await this.genAIClient.models.list();
|
|
423
|
+
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
421
426
|
}
|
|
422
|
-
|
|
423
|
-
type: "done",
|
|
424
|
-
fullText,
|
|
425
|
-
markdownFiles: [...this._markdownFiles],
|
|
426
|
-
usage: this.getLastUsage(),
|
|
427
|
-
warning: "Max tool rounds reached"
|
|
428
|
-
};
|
|
427
|
+
logger_default.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
429
428
|
}
|
|
430
429
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* @returns {
|
|
430
|
+
* Builds the options object for `genAIClient.chats.create()`.
|
|
431
|
+
* Override in subclasses to add tools, grounding, etc.
|
|
432
|
+
* @returns {Object}
|
|
433
|
+
* @protected
|
|
434
434
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
_getChatCreateOptions() {
|
|
436
|
+
return {
|
|
437
437
|
model: this.modelName,
|
|
438
438
|
config: {
|
|
439
439
|
...this.chatConfig,
|
|
440
440
|
...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
|
|
441
441
|
},
|
|
442
442
|
history: []
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// ── Chat Session Management ──────────────────────────────────────────────
|
|
446
|
+
/**
|
|
447
|
+
* Creates a new chat session with the given history.
|
|
448
|
+
* Internal helper used by init, seed, clearHistory, reset.
|
|
449
|
+
* @param {Array} [history=[]]
|
|
450
|
+
* @returns {Object} The new chat session
|
|
451
|
+
* @protected
|
|
452
|
+
*/
|
|
453
|
+
_createChatSession(history = []) {
|
|
454
|
+
const opts = this._getChatCreateOptions();
|
|
455
|
+
opts.history = history;
|
|
456
|
+
return this.genAIClient.chats.create(opts);
|
|
447
457
|
}
|
|
448
458
|
/**
|
|
449
|
-
*
|
|
459
|
+
* Retrieves the current conversation history.
|
|
450
460
|
* @param {boolean} [curated=false]
|
|
451
|
-
* @returns {
|
|
461
|
+
* @returns {Array<Object>}
|
|
452
462
|
*/
|
|
453
463
|
getHistory(curated = false) {
|
|
454
|
-
if (!this.chatSession)
|
|
464
|
+
if (!this.chatSession) {
|
|
465
|
+
logger_default.warn("Chat session not initialized. No history available.");
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
455
468
|
return this.chatSession.getHistory(curated);
|
|
456
469
|
}
|
|
457
470
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* @returns {
|
|
471
|
+
* Clears conversation history. Recreates chat session with empty history.
|
|
472
|
+
* Subclasses may override to preserve seeded examples.
|
|
473
|
+
* @returns {Promise<void>}
|
|
461
474
|
*/
|
|
462
|
-
|
|
463
|
-
if (!this.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
requestedModel: this.modelName,
|
|
472
|
-
timestamp: m.timestamp
|
|
473
|
-
};
|
|
475
|
+
async clearHistory() {
|
|
476
|
+
if (!this.chatSession) {
|
|
477
|
+
logger_default.warn(`Cannot clear history: chat not initialized.`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.chatSession = this._createChatSession([]);
|
|
481
|
+
this.lastResponseMetadata = null;
|
|
482
|
+
this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
|
|
483
|
+
logger_default.debug(`${this.constructor.name}: Conversation history cleared.`);
|
|
474
484
|
}
|
|
475
|
-
//
|
|
485
|
+
// ── Few-Shot Seeding ─────────────────────────────────────────────────────
|
|
476
486
|
/**
|
|
477
|
-
*
|
|
478
|
-
* @param {
|
|
479
|
-
* @
|
|
487
|
+
* Seeds the chat session with example input/output pairs for few-shot learning.
|
|
488
|
+
* @param {TransformationExample[]} examples - Array of example objects
|
|
489
|
+
* @param {Object} [opts={}] - Key configuration
|
|
490
|
+
* @param {string} [opts.promptKey='PROMPT'] - Key for input data in examples
|
|
491
|
+
* @param {string} [opts.answerKey='ANSWER'] - Key for output data in examples
|
|
492
|
+
* @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
|
|
493
|
+
* @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
|
|
494
|
+
* @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
|
|
495
|
+
* @returns {Promise<Array>} The updated chat history
|
|
496
|
+
*/
|
|
497
|
+
async seed(examples, opts = {}) {
|
|
498
|
+
await this.init();
|
|
499
|
+
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
500
|
+
logger_default.debug("No examples provided. Skipping seeding.");
|
|
501
|
+
return this.getHistory();
|
|
502
|
+
}
|
|
503
|
+
const promptKey = opts.promptKey || "PROMPT";
|
|
504
|
+
const answerKey = opts.answerKey || "ANSWER";
|
|
505
|
+
const contextKey = opts.contextKey || "CONTEXT";
|
|
506
|
+
const explanationKey = opts.explanationKey || "EXPLANATION";
|
|
507
|
+
const systemPromptKey = opts.systemPromptKey || "SYSTEM";
|
|
508
|
+
const instructionExample = examples.find((ex) => ex[systemPromptKey]);
|
|
509
|
+
if (instructionExample) {
|
|
510
|
+
logger_default.debug(`Found system prompt in examples; reinitializing chat.`);
|
|
511
|
+
this.systemPrompt = instructionExample[systemPromptKey];
|
|
512
|
+
this.chatConfig.systemInstruction = /** @type {string} */
|
|
513
|
+
this.systemPrompt;
|
|
514
|
+
await this.init(true);
|
|
515
|
+
}
|
|
516
|
+
logger_default.debug(`Seeding chat with ${examples.length} examples...`);
|
|
517
|
+
const historyToAdd = [];
|
|
518
|
+
for (const example of examples) {
|
|
519
|
+
const contextValue = example[contextKey] || "";
|
|
520
|
+
const promptValue = example[promptKey] || "";
|
|
521
|
+
const answerValue = example[answerKey] || "";
|
|
522
|
+
const explanationValue = example[explanationKey] || "";
|
|
523
|
+
let userText = "";
|
|
524
|
+
let modelResponse = {};
|
|
525
|
+
if (contextValue) {
|
|
526
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
527
|
+
userText += `CONTEXT:
|
|
528
|
+
${contextText}
|
|
529
|
+
|
|
530
|
+
`;
|
|
531
|
+
}
|
|
532
|
+
if (promptValue) {
|
|
533
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
534
|
+
userText += promptText;
|
|
535
|
+
}
|
|
536
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
537
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
538
|
+
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
539
|
+
if (userText.trim().length && modelText.trim().length > 0) {
|
|
540
|
+
historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
|
|
541
|
+
historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const currentHistory = this.chatSession?.getHistory() || [];
|
|
545
|
+
logger_default.debug(`Adding ${historyToAdd.length} items to chat history (${currentHistory.length} existing)...`);
|
|
546
|
+
this.chatSession = this._createChatSession([...currentHistory, ...historyToAdd]);
|
|
547
|
+
this.exampleCount = currentHistory.length + historyToAdd.length;
|
|
548
|
+
const newHistory = this.chatSession.getHistory();
|
|
549
|
+
logger_default.debug(`Chat session now has ${newHistory.length} history items.`);
|
|
550
|
+
return newHistory;
|
|
551
|
+
}
|
|
552
|
+
// ── Response Metadata ────────────────────────────────────────────────────
|
|
553
|
+
/**
|
|
554
|
+
* Captures response metadata (model version, token counts) from an API response.
|
|
555
|
+
* @param {Object} response - The API response object
|
|
556
|
+
* @protected
|
|
480
557
|
*/
|
|
481
558
|
_captureMetadata(response) {
|
|
482
559
|
this.lastResponseMetadata = {
|
|
@@ -488,7 +565,74 @@ var AIAgent = class {
|
|
|
488
565
|
timestamp: Date.now()
|
|
489
566
|
};
|
|
490
567
|
}
|
|
491
|
-
/**
|
|
568
|
+
/**
|
|
569
|
+
* Returns structured usage data from the last API call for billing verification.
|
|
570
|
+
* Includes CUMULATIVE token counts across all retry attempts.
|
|
571
|
+
* @returns {UsageData|null} Usage data or null if no API call has been made.
|
|
572
|
+
*/
|
|
573
|
+
getLastUsage() {
|
|
574
|
+
if (!this.lastResponseMetadata) return null;
|
|
575
|
+
const meta = this.lastResponseMetadata;
|
|
576
|
+
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
|
|
577
|
+
const useCumulative = cumulative.attempts > 0;
|
|
578
|
+
return {
|
|
579
|
+
promptTokens: useCumulative ? cumulative.promptTokens : meta.promptTokens,
|
|
580
|
+
responseTokens: useCumulative ? cumulative.responseTokens : meta.responseTokens,
|
|
581
|
+
totalTokens: useCumulative ? cumulative.totalTokens : meta.totalTokens,
|
|
582
|
+
attempts: useCumulative ? cumulative.attempts : 1,
|
|
583
|
+
modelVersion: meta.modelVersion,
|
|
584
|
+
requestedModel: meta.requestedModel,
|
|
585
|
+
timestamp: meta.timestamp
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// ── Token Estimation ─────────────────────────────────────────────────────
|
|
589
|
+
/**
|
|
590
|
+
* Estimates INPUT token count for a payload before sending.
|
|
591
|
+
* Includes system prompt + chat history + your new message.
|
|
592
|
+
* @param {Object|string} nextPayload - The next message to estimate
|
|
593
|
+
* @returns {Promise<{ inputTokens: number }>}
|
|
594
|
+
*/
|
|
595
|
+
async estimate(nextPayload) {
|
|
596
|
+
const contents = [];
|
|
597
|
+
if (this.systemPrompt) {
|
|
598
|
+
contents.push({ parts: [{ text: this.systemPrompt }] });
|
|
599
|
+
}
|
|
600
|
+
if (this.chatSession && typeof this.chatSession.getHistory === "function") {
|
|
601
|
+
const history = this.chatSession.getHistory();
|
|
602
|
+
if (Array.isArray(history) && history.length > 0) {
|
|
603
|
+
contents.push(...history);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
|
|
607
|
+
contents.push({ parts: [{ text: nextMessage }] });
|
|
608
|
+
const resp = await this.genAIClient.models.countTokens({
|
|
609
|
+
model: this.modelName,
|
|
610
|
+
contents
|
|
611
|
+
});
|
|
612
|
+
return { inputTokens: resp.totalTokens };
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Estimates the INPUT cost of sending a payload based on model pricing.
|
|
616
|
+
* @param {Object|string} nextPayload - The next message to estimate
|
|
617
|
+
* @returns {Promise<Object>} Cost estimation
|
|
618
|
+
*/
|
|
619
|
+
async estimateCost(nextPayload) {
|
|
620
|
+
const tokenInfo = await this.estimate(nextPayload);
|
|
621
|
+
const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
|
|
622
|
+
return {
|
|
623
|
+
inputTokens: tokenInfo.inputTokens,
|
|
624
|
+
model: this.modelName,
|
|
625
|
+
pricing,
|
|
626
|
+
estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
|
|
627
|
+
note: "Cost is for input tokens only; output cost depends on response length"
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// ── Private Helpers ──────────────────────────────────────────────────────
|
|
631
|
+
/**
|
|
632
|
+
* Configures the log level based on options, env vars, or NODE_ENV.
|
|
633
|
+
* @param {string} [logLevel]
|
|
634
|
+
* @private
|
|
635
|
+
*/
|
|
492
636
|
_configureLogLevel(logLevel) {
|
|
493
637
|
if (logLevel) {
|
|
494
638
|
if (logLevel === "none") {
|
|
@@ -508,12 +652,17 @@ var AIAgent = class {
|
|
|
508
652
|
logger_default.level = "info";
|
|
509
653
|
}
|
|
510
654
|
}
|
|
511
|
-
/**
|
|
655
|
+
/**
|
|
656
|
+
* Configures thinking settings based on model support.
|
|
657
|
+
* @param {Object|null|undefined} thinkingConfig
|
|
658
|
+
* @private
|
|
659
|
+
*/
|
|
512
660
|
_configureThinking(thinkingConfig) {
|
|
513
661
|
const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some((p) => p.test(this.modelName));
|
|
514
662
|
if (thinkingConfig === void 0) return;
|
|
515
663
|
if (thinkingConfig === null) {
|
|
516
664
|
delete this.chatConfig.thinkingConfig;
|
|
665
|
+
logger_default.debug(`thinkingConfig set to null - removed from configuration`);
|
|
517
666
|
return;
|
|
518
667
|
}
|
|
519
668
|
if (!modelSupportsThinking) {
|
|
@@ -528,16 +677,11 @@ var AIAgent = class {
|
|
|
528
677
|
logger_default.debug(`Thinking config applied: ${JSON.stringify(config)}`);
|
|
529
678
|
}
|
|
530
679
|
};
|
|
531
|
-
var
|
|
680
|
+
var base_default = BaseGemini;
|
|
532
681
|
|
|
533
|
-
//
|
|
534
|
-
var
|
|
535
|
-
|
|
536
|
-
var { NODE_ENV: NODE_ENV2 = "unknown", GEMINI_API_KEY, LOG_LEVEL: LOG_LEVEL2 = "" } = process.env;
|
|
537
|
-
var DEFAULT_SAFETY_SETTINGS2 = [
|
|
538
|
-
{ category: import_genai2.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: import_genai2.HarmBlockThreshold.BLOCK_NONE },
|
|
539
|
-
{ category: import_genai2.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: import_genai2.HarmBlockThreshold.BLOCK_NONE }
|
|
540
|
-
];
|
|
682
|
+
// transformer.js
|
|
683
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
684
|
+
var import_path = __toESM(require("path"), 1);
|
|
541
685
|
var DEFAULT_SYSTEM_INSTRUCTIONS = `
|
|
542
686
|
You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
|
|
543
687
|
|
|
@@ -551,458 +695,210 @@ Always respond ONLY with a valid JSON object that strictly adheres to the expect
|
|
|
551
695
|
|
|
552
696
|
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
553
697
|
`;
|
|
554
|
-
var
|
|
555
|
-
thinkingBudget: 0
|
|
556
|
-
};
|
|
557
|
-
var DEFAULT_MAX_OUTPUT_TOKENS = 5e4;
|
|
558
|
-
var THINKING_SUPPORTED_MODELS2 = [
|
|
559
|
-
/^gemini-3-flash(-preview)?$/,
|
|
560
|
-
/^gemini-3-pro(-preview|-image-preview)?$/,
|
|
561
|
-
/^gemini-2\.5-pro/,
|
|
562
|
-
/^gemini-2\.5-flash(-preview)?$/,
|
|
563
|
-
/^gemini-2\.5-flash-lite(-preview)?$/,
|
|
564
|
-
/^gemini-2\.0-flash$/
|
|
565
|
-
// Experimental support, exact match only
|
|
566
|
-
];
|
|
567
|
-
var DEFAULT_CHAT_CONFIG = {
|
|
568
|
-
responseMimeType: "application/json",
|
|
569
|
-
temperature: 0.2,
|
|
570
|
-
topP: 0.95,
|
|
571
|
-
topK: 64,
|
|
572
|
-
systemInstruction: DEFAULT_SYSTEM_INSTRUCTIONS,
|
|
573
|
-
safetySettings: DEFAULT_SAFETY_SETTINGS2
|
|
574
|
-
};
|
|
575
|
-
var AITransformer = class {
|
|
698
|
+
var Transformer = class extends base_default {
|
|
576
699
|
/**
|
|
577
|
-
* @param {
|
|
578
|
-
*
|
|
700
|
+
* @param {TransformerOptions} [options={}]
|
|
579
701
|
*/
|
|
580
702
|
constructor(options = {}) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
this.answerKey = "";
|
|
584
|
-
this.contextKey = "";
|
|
585
|
-
this.explanationKey = "";
|
|
586
|
-
this.systemInstructionKey = "";
|
|
587
|
-
this.maxRetries = 3;
|
|
588
|
-
this.retryDelay = 1e3;
|
|
589
|
-
this.chatConfig = {};
|
|
590
|
-
this.apiKey = GEMINI_API_KEY;
|
|
591
|
-
this.onlyJSON = true;
|
|
592
|
-
this.asyncValidator = null;
|
|
593
|
-
this.logLevel = "info";
|
|
594
|
-
this.lastResponseMetadata = null;
|
|
595
|
-
this.exampleCount = 0;
|
|
596
|
-
this._cumulativeUsage = {
|
|
597
|
-
promptTokens: 0,
|
|
598
|
-
responseTokens: 0,
|
|
599
|
-
totalTokens: 0,
|
|
600
|
-
attempts: 0
|
|
601
|
-
};
|
|
602
|
-
AITransformFactory.call(this, options);
|
|
603
|
-
this.init = initChat.bind(this);
|
|
604
|
-
this.seed = seedWithExamples.bind(this);
|
|
605
|
-
this.rawMessage = rawMessage.bind(this);
|
|
606
|
-
this.message = (payload, opts = {}, validatorFn = null) => {
|
|
607
|
-
return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
|
|
608
|
-
};
|
|
609
|
-
this.rebuild = rebuildPayload.bind(this);
|
|
610
|
-
this.reset = resetChat.bind(this);
|
|
611
|
-
this.getHistory = getChatHistory.bind(this);
|
|
612
|
-
this.messageAndValidate = prepareAndValidateMessage.bind(this);
|
|
613
|
-
this.transformWithValidation = prepareAndValidateMessage.bind(this);
|
|
614
|
-
this.estimate = estimateInputTokens.bind(this);
|
|
615
|
-
this.updateSystemInstructions = updateSystemInstructions.bind(this);
|
|
616
|
-
this.estimateCost = estimateCost.bind(this);
|
|
617
|
-
this.clearConversation = clearConversation.bind(this);
|
|
618
|
-
this.getLastUsage = getLastUsage.bind(this);
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
var index_default = AITransformer;
|
|
622
|
-
function AITransformFactory(options = {}) {
|
|
623
|
-
this.modelName = options.modelName || "gemini-2.5-flash";
|
|
624
|
-
if (options.systemInstructions === void 0) {
|
|
625
|
-
this.systemInstructions = DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
626
|
-
} else {
|
|
627
|
-
this.systemInstructions = options.systemInstructions;
|
|
628
|
-
}
|
|
629
|
-
if (options.logLevel) {
|
|
630
|
-
this.logLevel = options.logLevel;
|
|
631
|
-
if (this.logLevel === "none") {
|
|
632
|
-
logger_default.level = "silent";
|
|
633
|
-
} else {
|
|
634
|
-
logger_default.level = this.logLevel;
|
|
635
|
-
}
|
|
636
|
-
} else if (LOG_LEVEL2) {
|
|
637
|
-
this.logLevel = LOG_LEVEL2;
|
|
638
|
-
logger_default.level = LOG_LEVEL2;
|
|
639
|
-
} else if (NODE_ENV2 === "dev") {
|
|
640
|
-
this.logLevel = "debug";
|
|
641
|
-
logger_default.level = "debug";
|
|
642
|
-
} else if (NODE_ENV2 === "test") {
|
|
643
|
-
this.logLevel = "warn";
|
|
644
|
-
logger_default.level = "warn";
|
|
645
|
-
} else if (NODE_ENV2.startsWith("prod")) {
|
|
646
|
-
this.logLevel = "error";
|
|
647
|
-
logger_default.level = "error";
|
|
648
|
-
} else {
|
|
649
|
-
this.logLevel = "info";
|
|
650
|
-
logger_default.level = "info";
|
|
651
|
-
}
|
|
652
|
-
this.vertexai = options.vertexai || false;
|
|
653
|
-
this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
|
|
654
|
-
this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || void 0;
|
|
655
|
-
this.googleAuthOptions = options.googleAuthOptions || null;
|
|
656
|
-
this.apiKey = options.apiKey !== void 0 && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
|
|
657
|
-
if (!this.vertexai && !this.apiKey) {
|
|
658
|
-
throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var. For Vertex AI, set vertexai: true with project and location.");
|
|
659
|
-
}
|
|
660
|
-
if (this.vertexai && !this.project) {
|
|
661
|
-
throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
|
|
662
|
-
}
|
|
663
|
-
this.chatConfig = {
|
|
664
|
-
...DEFAULT_CHAT_CONFIG,
|
|
665
|
-
...options.chatConfig
|
|
666
|
-
};
|
|
667
|
-
if (this.systemInstructions) {
|
|
668
|
-
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
669
|
-
} else if (options.systemInstructions !== void 0) {
|
|
670
|
-
delete this.chatConfig.systemInstruction;
|
|
671
|
-
}
|
|
672
|
-
if (options.maxOutputTokens !== void 0) {
|
|
673
|
-
if (options.maxOutputTokens === null) {
|
|
674
|
-
delete this.chatConfig.maxOutputTokens;
|
|
675
|
-
} else {
|
|
676
|
-
this.chatConfig.maxOutputTokens = options.maxOutputTokens;
|
|
703
|
+
if (options.systemPrompt === void 0) {
|
|
704
|
+
options = { ...options, systemPrompt: DEFAULT_SYSTEM_INSTRUCTIONS };
|
|
677
705
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
this.chatConfig.
|
|
706
|
+
super(options);
|
|
707
|
+
this.chatConfig.responseMimeType = "application/json";
|
|
708
|
+
this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
|
|
709
|
+
if (options.responseSchema) {
|
|
710
|
+
this.chatConfig.responseSchema = options.responseSchema;
|
|
683
711
|
}
|
|
684
|
-
|
|
685
|
-
this.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
} else {
|
|
709
|
-
if (logger_default.level !== "silent") {
|
|
710
|
-
logger_default.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
|
|
711
|
-
}
|
|
712
|
+
this.promptKey = options.promptKey || options.sourceKey || "PROMPT";
|
|
713
|
+
this.answerKey = options.answerKey || options.targetKey || "ANSWER";
|
|
714
|
+
this.contextKey = options.contextKey || "CONTEXT";
|
|
715
|
+
this.explanationKey = options.explanationKey || "EXPLANATION";
|
|
716
|
+
this.systemPromptKey = options.systemPromptKey || "SYSTEM";
|
|
717
|
+
if (this.promptKey === this.answerKey) {
|
|
718
|
+
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
719
|
+
}
|
|
720
|
+
this.examplesFile = options.examplesFile || null;
|
|
721
|
+
this.exampleData = options.exampleData || null;
|
|
722
|
+
this.asyncValidator = options.asyncValidator || null;
|
|
723
|
+
this.maxRetries = options.maxRetries || 3;
|
|
724
|
+
this.retryDelay = options.retryDelay || 1e3;
|
|
725
|
+
this.enableGrounding = options.enableGrounding || false;
|
|
726
|
+
this.groundingConfig = options.groundingConfig || {};
|
|
727
|
+
logger_default.debug(`Transformer keys \u2014 Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
728
|
+
}
|
|
729
|
+
// ── Chat Create Options Override ──────────────────────────────────────────
|
|
730
|
+
/** @protected */
|
|
731
|
+
_getChatCreateOptions() {
|
|
732
|
+
const opts = super._getChatCreateOptions();
|
|
733
|
+
if (this.enableGrounding) {
|
|
734
|
+
opts.config.tools = [{ googleSearch: this.groundingConfig }];
|
|
735
|
+
logger_default.debug(`Search grounding ENABLED (WARNING: costs $35/1k queries)`);
|
|
712
736
|
}
|
|
737
|
+
return opts;
|
|
713
738
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
|
|
743
|
-
logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
744
|
-
logger_default.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
|
|
745
|
-
if (this.vertexai) {
|
|
746
|
-
logger_default.debug(`Using Vertex AI - Project: ${this.project}, Location: ${this.location || "global (default)"}`);
|
|
747
|
-
if (this.googleAuthOptions?.keyFilename) {
|
|
748
|
-
logger_default.debug(`Auth: Service account key file: ${this.googleAuthOptions.keyFilename}`);
|
|
749
|
-
} else if (this.googleAuthOptions?.credentials) {
|
|
750
|
-
logger_default.debug(`Auth: Inline credentials provided`);
|
|
739
|
+
// ── Seeding ──────────────────────────────────────────────────────────────
|
|
740
|
+
/**
|
|
741
|
+
* Seeds the chat with transformation examples using the configured key mapping.
|
|
742
|
+
* Overrides base seed() to use Transformer-specific keys and support
|
|
743
|
+
* examplesFile/exampleData fallbacks.
|
|
744
|
+
*
|
|
745
|
+
* @param {TransformationExample[]} [examples] - Array of example objects
|
|
746
|
+
* @returns {Promise<Array>} The updated chat history
|
|
747
|
+
*/
|
|
748
|
+
async seed(examples) {
|
|
749
|
+
await this.init();
|
|
750
|
+
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
751
|
+
if (this.examplesFile) {
|
|
752
|
+
logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
753
|
+
try {
|
|
754
|
+
const filePath = import_path.default.resolve(this.examplesFile);
|
|
755
|
+
const raw = await import_promises.default.readFile(filePath, "utf-8");
|
|
756
|
+
examples = JSON.parse(raw);
|
|
757
|
+
} catch (err) {
|
|
758
|
+
throw new Error(`Could not load examples from file: ${this.examplesFile}. ${err.message}`);
|
|
759
|
+
}
|
|
760
|
+
} else if (this.exampleData) {
|
|
761
|
+
logger_default.debug(`Using example data provided in options.`);
|
|
762
|
+
if (Array.isArray(this.exampleData)) {
|
|
763
|
+
examples = this.exampleData;
|
|
764
|
+
} else {
|
|
765
|
+
throw new Error(`Invalid example data provided. Expected an array of examples.`);
|
|
766
|
+
}
|
|
751
767
|
} else {
|
|
752
|
-
logger_default.debug(
|
|
768
|
+
logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
769
|
+
return this.getHistory();
|
|
753
770
|
}
|
|
754
|
-
} else {
|
|
755
|
-
logger_default.debug(`Using Gemini API with key: ${this.apiKey.substring(0, 10)}...`);
|
|
756
|
-
}
|
|
757
|
-
logger_default.debug(`Grounding ${this.enableGrounding ? "ENABLED" : "DISABLED"} (costs $35/1k queries)`);
|
|
758
|
-
}
|
|
759
|
-
const clientOptions = this.vertexai ? {
|
|
760
|
-
vertexai: true,
|
|
761
|
-
project: this.project,
|
|
762
|
-
...this.location && { location: this.location },
|
|
763
|
-
...this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions }
|
|
764
|
-
} : { apiKey: this.apiKey };
|
|
765
|
-
const ai = new import_genai2.GoogleGenAI(clientOptions);
|
|
766
|
-
this.genAIClient = ai;
|
|
767
|
-
this.chat = null;
|
|
768
|
-
}
|
|
769
|
-
async function initChat(force = false) {
|
|
770
|
-
if (this.chat && !force) return;
|
|
771
|
-
logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
772
|
-
const chatOptions = {
|
|
773
|
-
model: this.modelName,
|
|
774
|
-
// @ts-ignore
|
|
775
|
-
config: {
|
|
776
|
-
...this.chatConfig,
|
|
777
|
-
...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
|
|
778
|
-
},
|
|
779
|
-
history: []
|
|
780
|
-
};
|
|
781
|
-
if (this.enableGrounding) {
|
|
782
|
-
chatOptions.config.tools = [{
|
|
783
|
-
googleSearch: this.groundingConfig
|
|
784
|
-
}];
|
|
785
|
-
logger_default.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
|
|
786
|
-
}
|
|
787
|
-
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
788
|
-
try {
|
|
789
|
-
await this.genAIClient.models.list();
|
|
790
|
-
logger_default.debug("Gemini API connection successful.");
|
|
791
|
-
} catch (e) {
|
|
792
|
-
throw new Error(`Gemini chat initialization failed: ${e.message}`);
|
|
793
|
-
}
|
|
794
|
-
logger_default.debug("Gemini chat session initialized.");
|
|
795
|
-
}
|
|
796
|
-
async function seedWithExamples(examples) {
|
|
797
|
-
await this.init();
|
|
798
|
-
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
799
|
-
if (this.examplesFile) {
|
|
800
|
-
logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
801
|
-
try {
|
|
802
|
-
examples = await import_ak_tools.default.load(import_path.default.resolve(this.examplesFile), true);
|
|
803
|
-
} catch (err) {
|
|
804
|
-
throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
|
|
805
|
-
}
|
|
806
|
-
} else if (this.exampleData) {
|
|
807
|
-
logger_default.debug(`Using example data provided in options.`);
|
|
808
|
-
if (Array.isArray(this.exampleData)) {
|
|
809
|
-
examples = this.exampleData;
|
|
810
|
-
} else {
|
|
811
|
-
throw new Error(`Invalid example data provided. Expected an array of examples.`);
|
|
812
|
-
}
|
|
813
|
-
} else {
|
|
814
|
-
logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const instructionExample = examples.find((ex) => ex[this.systemInstructionsKey]);
|
|
819
|
-
if (instructionExample) {
|
|
820
|
-
logger_default.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
821
|
-
this.systemInstructions = instructionExample[this.systemInstructionsKey];
|
|
822
|
-
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
823
|
-
await this.init(true);
|
|
824
|
-
}
|
|
825
|
-
logger_default.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
826
|
-
const historyToAdd = [];
|
|
827
|
-
for (const example of examples) {
|
|
828
|
-
const contextValue = example[this.contextKey] || "";
|
|
829
|
-
const promptValue = example[this.promptKey] || "";
|
|
830
|
-
const answerValue = example[this.answerKey] || "";
|
|
831
|
-
const explanationValue = example[this.explanationKey] || "";
|
|
832
|
-
let userText = "";
|
|
833
|
-
let modelResponse = {};
|
|
834
|
-
if (contextValue) {
|
|
835
|
-
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
836
|
-
userText += `CONTEXT:
|
|
837
|
-
${contextText}
|
|
838
|
-
|
|
839
|
-
`;
|
|
840
771
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (userText.trim().length && modelText.trim().length > 0) {
|
|
849
|
-
historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
|
|
850
|
-
historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const currentHistory = this?.chat?.getHistory() || [];
|
|
854
|
-
logger_default.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
|
|
855
|
-
this.chat = await this.genAIClient.chats.create({
|
|
856
|
-
model: this.modelName,
|
|
857
|
-
// @ts-ignore
|
|
858
|
-
config: {
|
|
859
|
-
...this.chatConfig,
|
|
860
|
-
...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
|
|
861
|
-
},
|
|
862
|
-
history: [...currentHistory, ...historyToAdd]
|
|
863
|
-
});
|
|
864
|
-
this.exampleCount = currentHistory.length + historyToAdd.length;
|
|
865
|
-
const newHistory = this.chat.getHistory();
|
|
866
|
-
logger_default.debug(`Created new chat session with ${newHistory.length} examples.`);
|
|
867
|
-
return newHistory;
|
|
868
|
-
}
|
|
869
|
-
async function rawMessage(sourcePayload, messageOptions = {}) {
|
|
870
|
-
if (!this.chat) {
|
|
871
|
-
throw new Error("Chat session not initialized.");
|
|
772
|
+
return await super.seed(examples, {
|
|
773
|
+
promptKey: this.promptKey,
|
|
774
|
+
answerKey: this.answerKey,
|
|
775
|
+
contextKey: this.contextKey,
|
|
776
|
+
explanationKey: this.explanationKey,
|
|
777
|
+
systemPromptKey: this.systemPromptKey
|
|
778
|
+
});
|
|
872
779
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
780
|
+
// ── Primary Send Method ──────────────────────────────────────────────────
|
|
781
|
+
/**
|
|
782
|
+
* Transforms a payload using the seeded examples and model.
|
|
783
|
+
* Includes validation and automatic retry with AI-powered error correction.
|
|
784
|
+
*
|
|
785
|
+
* @param {Object|string} payload - The source payload to transform
|
|
786
|
+
* @param {import('./types').SendOptions} [opts={}] - Per-message options
|
|
787
|
+
* @param {AsyncValidatorFunction|null} [validatorFn] - Validator for this call (overrides constructor validator)
|
|
788
|
+
* @returns {Promise<Object>} The transformed payload
|
|
789
|
+
*/
|
|
790
|
+
async send(payload, opts = {}, validatorFn = null) {
|
|
791
|
+
if (!this.chatSession) {
|
|
792
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
880
793
|
}
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
requestedModel: this.modelName,
|
|
885
|
-
promptTokens: result.usageMetadata?.promptTokenCount || 0,
|
|
886
|
-
responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
|
|
887
|
-
totalTokens: result.usageMetadata?.totalTokenCount || 0,
|
|
888
|
-
timestamp: Date.now()
|
|
889
|
-
};
|
|
890
|
-
if (result.usageMetadata && logger_default.level !== "silent") {
|
|
891
|
-
logger_default.debug(`API response metadata: ${JSON.stringify({
|
|
892
|
-
modelVersion: result.modelVersion || "not-provided",
|
|
893
|
-
requestedModel: this.modelName,
|
|
894
|
-
promptTokens: result.usageMetadata.promptTokenCount,
|
|
895
|
-
responseTokens: result.usageMetadata.candidatesTokenCount,
|
|
896
|
-
totalTokens: result.usageMetadata.totalTokenCount
|
|
897
|
-
})}`);
|
|
794
|
+
const validator = validatorFn || this.asyncValidator;
|
|
795
|
+
if (opts.stateless) {
|
|
796
|
+
return await this._statelessSend(payload, opts, validator);
|
|
898
797
|
}
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
if (
|
|
902
|
-
|
|
798
|
+
const maxRetries = opts.maxRetries ?? this.maxRetries;
|
|
799
|
+
const retryDelay = opts.retryDelay ?? this.retryDelay;
|
|
800
|
+
if (opts.enableGrounding !== void 0 && opts.enableGrounding !== this.enableGrounding) {
|
|
801
|
+
const originalGrounding = this.enableGrounding;
|
|
802
|
+
const originalConfig = this.groundingConfig;
|
|
803
|
+
try {
|
|
804
|
+
this.enableGrounding = opts.enableGrounding;
|
|
805
|
+
this.groundingConfig = opts.groundingConfig ?? this.groundingConfig;
|
|
806
|
+
await this.init(true);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
this.enableGrounding = originalGrounding;
|
|
809
|
+
this.groundingConfig = originalConfig;
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
opts._restoreGrounding = async () => {
|
|
813
|
+
this.enableGrounding = originalGrounding;
|
|
814
|
+
this.groundingConfig = originalConfig;
|
|
815
|
+
await this.init(true);
|
|
816
|
+
};
|
|
903
817
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
818
|
+
let lastPayload = this._preparePayload(payload);
|
|
819
|
+
const messageOptions = {};
|
|
820
|
+
if (opts.labels) messageOptions.labels = opts.labels;
|
|
821
|
+
this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
|
|
822
|
+
let lastError = null;
|
|
823
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
824
|
+
try {
|
|
825
|
+
const transformedPayload = attempt === 0 ? await this.rawSend(lastPayload, messageOptions) : await this.rebuild(lastPayload, lastError.message);
|
|
826
|
+
if (this.lastResponseMetadata) {
|
|
827
|
+
this._cumulativeUsage.promptTokens += this.lastResponseMetadata.promptTokens || 0;
|
|
828
|
+
this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
|
|
829
|
+
this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
|
|
830
|
+
this._cumulativeUsage.attempts = attempt + 1;
|
|
831
|
+
}
|
|
832
|
+
lastPayload = transformedPayload;
|
|
833
|
+
if (validator) {
|
|
834
|
+
await validator(transformedPayload);
|
|
835
|
+
}
|
|
836
|
+
logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
837
|
+
if (opts._restoreGrounding) await opts._restoreGrounding();
|
|
838
|
+
return transformedPayload;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
lastError = error;
|
|
841
|
+
logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
842
|
+
if (attempt >= maxRetries) {
|
|
843
|
+
logger_default.error(`All ${maxRetries + 1} attempts failed.`);
|
|
844
|
+
if (opts._restoreGrounding) await opts._restoreGrounding();
|
|
845
|
+
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
846
|
+
}
|
|
847
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
848
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
849
|
+
}
|
|
908
850
|
}
|
|
909
|
-
throw new Error(`Transformation failed: ${error.message}`);
|
|
910
851
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (enableGroundingForMessage !== this.enableGrounding) {
|
|
924
|
-
const originalGrounding = this.enableGrounding;
|
|
925
|
-
const originalConfig = this.groundingConfig;
|
|
926
|
-
try {
|
|
927
|
-
this.enableGrounding = enableGroundingForMessage;
|
|
928
|
-
this.groundingConfig = groundingConfigForMessage;
|
|
929
|
-
await this.init(true);
|
|
930
|
-
if (enableGroundingForMessage) {
|
|
931
|
-
logger_default.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
|
|
932
|
-
} else {
|
|
933
|
-
logger_default.debug(`Search grounding DISABLED for this message`);
|
|
934
|
-
}
|
|
935
|
-
} catch (error) {
|
|
936
|
-
this.enableGrounding = originalGrounding;
|
|
937
|
-
this.groundingConfig = originalConfig;
|
|
938
|
-
throw error;
|
|
852
|
+
// ── Raw Send ─────────────────────────────────────────────────────────────
|
|
853
|
+
/**
|
|
854
|
+
* Sends a single prompt to the model and parses the JSON response.
|
|
855
|
+
* No validation or retry logic.
|
|
856
|
+
*
|
|
857
|
+
* @param {Object|string} payload - The source payload
|
|
858
|
+
* @param {Object} [messageOptions={}] - Per-message options (e.g., labels)
|
|
859
|
+
* @returns {Promise<Object>} The transformed payload
|
|
860
|
+
*/
|
|
861
|
+
async rawSend(payload, messageOptions = {}) {
|
|
862
|
+
if (!this.chatSession) {
|
|
863
|
+
throw new Error("Chat session not initialized.");
|
|
939
864
|
}
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
await this.init(true);
|
|
944
|
-
};
|
|
945
|
-
options._restoreGrounding = restoreGrounding;
|
|
946
|
-
}
|
|
947
|
-
let lastError = null;
|
|
948
|
-
let lastPayload = null;
|
|
949
|
-
if (sourcePayload && isJSON(sourcePayload)) {
|
|
950
|
-
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
951
|
-
} else if (typeof sourcePayload === "string") {
|
|
952
|
-
lastPayload = sourcePayload;
|
|
953
|
-
} else if (typeof sourcePayload === "boolean" || typeof sourcePayload === "number") {
|
|
954
|
-
lastPayload = sourcePayload.toString();
|
|
955
|
-
} else if (sourcePayload === null || sourcePayload === void 0) {
|
|
956
|
-
lastPayload = JSON.stringify({});
|
|
957
|
-
} else {
|
|
958
|
-
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
959
|
-
}
|
|
960
|
-
const messageOptions = {};
|
|
961
|
-
if (options.labels) {
|
|
962
|
-
messageOptions.labels = options.labels;
|
|
963
|
-
}
|
|
964
|
-
this._cumulativeUsage = {
|
|
965
|
-
promptTokens: 0,
|
|
966
|
-
responseTokens: 0,
|
|
967
|
-
totalTokens: 0,
|
|
968
|
-
attempts: 0
|
|
969
|
-
};
|
|
970
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
865
|
+
const actualPayload = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
866
|
+
const mergedLabels = { ...this.labels, ...messageOptions.labels || {} };
|
|
867
|
+
const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
|
|
971
868
|
try {
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
|
|
976
|
-
this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
|
|
977
|
-
this._cumulativeUsage.attempts = attempt + 1;
|
|
869
|
+
const sendParams = { message: actualPayload };
|
|
870
|
+
if (hasLabels) {
|
|
871
|
+
sendParams.config = { labels: mergedLabels };
|
|
978
872
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
873
|
+
const result = await this.chatSession.sendMessage(sendParams);
|
|
874
|
+
this._captureMetadata(result);
|
|
875
|
+
if (result.usageMetadata && logger_default.level !== "silent") {
|
|
876
|
+
logger_default.debug(`API response: model=${result.modelVersion || "unknown"}, tokens=${result.usageMetadata.totalTokenCount}`);
|
|
982
877
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
878
|
+
const modelResponse = result.text;
|
|
879
|
+
const extractedJSON = extractJSON(modelResponse);
|
|
880
|
+
if (extractedJSON?.data) {
|
|
881
|
+
return extractedJSON.data;
|
|
986
882
|
}
|
|
987
|
-
return
|
|
883
|
+
return extractedJSON;
|
|
988
884
|
} catch (error) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
if (attempt >= maxRetries) {
|
|
992
|
-
logger_default.error(`All ${maxRetries + 1} attempts failed.`);
|
|
993
|
-
if (options._restoreGrounding) {
|
|
994
|
-
await options._restoreGrounding();
|
|
995
|
-
}
|
|
996
|
-
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
885
|
+
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
886
|
+
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
997
887
|
}
|
|
998
|
-
|
|
999
|
-
await new Promise((res) => setTimeout(res, delay));
|
|
888
|
+
throw new Error(`Transformation failed: ${error.message}`);
|
|
1000
889
|
}
|
|
1001
890
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
891
|
+
// ── Rebuild ──────────────────────────────────────────────────────────────
|
|
892
|
+
/**
|
|
893
|
+
* Asks the model to fix a payload that failed validation.
|
|
894
|
+
*
|
|
895
|
+
* @param {Object} lastPayload - The payload that failed
|
|
896
|
+
* @param {string} serverError - The error message
|
|
897
|
+
* @returns {Promise<Object>} Corrected payload
|
|
898
|
+
*/
|
|
899
|
+
async rebuild(lastPayload, serverError) {
|
|
900
|
+
await this.init();
|
|
901
|
+
const prompt = `
|
|
1006
902
|
The previous JSON payload (below) failed validation.
|
|
1007
903
|
The server's error message is quoted afterward.
|
|
1008
904
|
|
|
@@ -1016,512 +912,1331 @@ ${serverError}
|
|
|
1016
912
|
Please return a NEW JSON payload that corrects the issue.
|
|
1017
913
|
Respond with JSON only \u2013 no comments or explanations.
|
|
1018
914
|
`;
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
logger_default.debug(`Rebuild response metadata - tokens used: ${result.usageMetadata.totalTokenCount}`);
|
|
915
|
+
let result;
|
|
916
|
+
try {
|
|
917
|
+
result = await this.chatSession.sendMessage({ message: prompt });
|
|
918
|
+
this._captureMetadata(result);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
const text = result.text ?? result.response ?? "";
|
|
924
|
+
return typeof text === "object" ? text : JSON.parse(text);
|
|
925
|
+
} catch (parseErr) {
|
|
926
|
+
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
1032
927
|
}
|
|
1033
|
-
} catch (err) {
|
|
1034
|
-
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
1035
|
-
}
|
|
1036
|
-
try {
|
|
1037
|
-
const text = result.text ?? result.response ?? "";
|
|
1038
|
-
return typeof text === "object" ? text : JSON.parse(text);
|
|
1039
|
-
} catch (parseErr) {
|
|
1040
|
-
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
1041
928
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
"gemini-3-pro": { input: 2, output: 12 },
|
|
1067
|
-
"gemini-3-pro-preview": { input: 2, output: 12 },
|
|
1068
|
-
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
1069
|
-
"gemini-2.0-flash-lite": { input: 0.02, output: 0.1 }
|
|
1070
|
-
};
|
|
1071
|
-
async function estimateCost(nextPayload) {
|
|
1072
|
-
const tokenInfo = await this.estimate(nextPayload);
|
|
1073
|
-
const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
|
|
1074
|
-
return {
|
|
1075
|
-
inputTokens: tokenInfo.inputTokens,
|
|
1076
|
-
model: this.modelName,
|
|
1077
|
-
pricing,
|
|
1078
|
-
estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
|
|
1079
|
-
note: "Cost is for input tokens only; output cost depends on response length"
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
async function resetChat() {
|
|
1083
|
-
if (this.chat) {
|
|
1084
|
-
logger_default.debug("Resetting Gemini chat session...");
|
|
1085
|
-
const chatOptions = {
|
|
929
|
+
// ── Stateless Send ───────────────────────────────────────────────────────
|
|
930
|
+
/**
|
|
931
|
+
* Sends a one-off message using generateContent (not chat).
|
|
932
|
+
* Does NOT affect chat history.
|
|
933
|
+
* @param {Object|string} payload
|
|
934
|
+
* @param {Object} [opts={}]
|
|
935
|
+
* @param {AsyncValidatorFunction|null} [validatorFn]
|
|
936
|
+
* @returns {Promise<Object>}
|
|
937
|
+
* @private
|
|
938
|
+
*/
|
|
939
|
+
async _statelessSend(payload, opts = {}, validatorFn = null) {
|
|
940
|
+
if (!this.chatSession) {
|
|
941
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
942
|
+
}
|
|
943
|
+
const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
944
|
+
const contents = [];
|
|
945
|
+
if (this.exampleCount > 0) {
|
|
946
|
+
const history = this.chatSession.getHistory();
|
|
947
|
+
const exampleHistory = history.slice(0, this.exampleCount);
|
|
948
|
+
contents.push(...exampleHistory);
|
|
949
|
+
}
|
|
950
|
+
contents.push({ role: "user", parts: [{ text: payloadStr }] });
|
|
951
|
+
const mergedLabels = { ...this.labels, ...opts.labels || {} };
|
|
952
|
+
const result = await this.genAIClient.models.generateContent({
|
|
1086
953
|
model: this.modelName,
|
|
1087
|
-
|
|
954
|
+
contents,
|
|
1088
955
|
config: {
|
|
1089
956
|
...this.chatConfig,
|
|
1090
|
-
...this.vertexai && Object.keys(
|
|
1091
|
-
}
|
|
1092
|
-
|
|
957
|
+
...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
this._captureMetadata(result);
|
|
961
|
+
this._cumulativeUsage = {
|
|
962
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
963
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
964
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
965
|
+
attempts: 1
|
|
1093
966
|
};
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
967
|
+
const modelResponse = result.text;
|
|
968
|
+
const extractedJSON = extractJSON(modelResponse);
|
|
969
|
+
let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
|
|
970
|
+
if (validatorFn) {
|
|
971
|
+
await validatorFn(transformedPayload);
|
|
1099
972
|
}
|
|
1100
|
-
|
|
1101
|
-
logger_default.debug("Chat session reset.");
|
|
1102
|
-
} else {
|
|
1103
|
-
logger_default.warn("Cannot reset chat session: chat not yet initialized.");
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
function getChatHistory() {
|
|
1107
|
-
if (!this.chat) {
|
|
1108
|
-
logger_default.warn("Chat session not initialized. No history available.");
|
|
1109
|
-
return [];
|
|
973
|
+
return transformedPayload;
|
|
1110
974
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const history = this.chat.getHistory();
|
|
1128
|
-
const exampleHistory = history.slice(0, this.exampleCount || 0);
|
|
1129
|
-
this.chat = await this.genAIClient.chats.create({
|
|
1130
|
-
model: this.modelName,
|
|
1131
|
-
// @ts-ignore
|
|
1132
|
-
config: {
|
|
1133
|
-
...this.chatConfig,
|
|
1134
|
-
...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
|
|
1135
|
-
},
|
|
1136
|
-
history: exampleHistory
|
|
1137
|
-
});
|
|
1138
|
-
this.lastResponseMetadata = null;
|
|
1139
|
-
this._cumulativeUsage = {
|
|
1140
|
-
promptTokens: 0,
|
|
1141
|
-
responseTokens: 0,
|
|
1142
|
-
totalTokens: 0,
|
|
1143
|
-
attempts: 0
|
|
1144
|
-
};
|
|
1145
|
-
logger_default.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
|
|
1146
|
-
}
|
|
1147
|
-
function getLastUsage() {
|
|
1148
|
-
if (!this.lastResponseMetadata) {
|
|
1149
|
-
return null;
|
|
1150
|
-
}
|
|
1151
|
-
const meta = this.lastResponseMetadata;
|
|
1152
|
-
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
|
|
1153
|
-
const useCumulative = cumulative.attempts > 0;
|
|
1154
|
-
return {
|
|
1155
|
-
// Token breakdown for billing - CUMULATIVE across all retry attempts
|
|
1156
|
-
promptTokens: useCumulative ? cumulative.promptTokens : meta.promptTokens,
|
|
1157
|
-
responseTokens: useCumulative ? cumulative.responseTokens : meta.responseTokens,
|
|
1158
|
-
totalTokens: useCumulative ? cumulative.totalTokens : meta.totalTokens,
|
|
1159
|
-
// Number of attempts (1 = success on first try, 2+ = retries were needed)
|
|
1160
|
-
attempts: useCumulative ? cumulative.attempts : 1,
|
|
1161
|
-
// Model verification for billing cross-check
|
|
1162
|
-
modelVersion: meta.modelVersion,
|
|
1163
|
-
// Actual model that responded (e.g., 'gemini-2.5-flash-001')
|
|
1164
|
-
requestedModel: meta.requestedModel,
|
|
1165
|
-
// Model you requested (e.g., 'gemini-2.5-flash')
|
|
1166
|
-
// Timestamp for audit trail
|
|
1167
|
-
timestamp: meta.timestamp
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
async function statelessMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
1171
|
-
if (!this.chat) {
|
|
1172
|
-
throw new Error("Chat session not initialized. Please call init() first.");
|
|
1173
|
-
}
|
|
1174
|
-
const payloadStr = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
|
|
1175
|
-
const contents = [];
|
|
1176
|
-
if (this.exampleCount > 0) {
|
|
1177
|
-
const history = this.chat.getHistory();
|
|
1178
|
-
const exampleHistory = history.slice(0, this.exampleCount);
|
|
1179
|
-
contents.push(...exampleHistory);
|
|
1180
|
-
}
|
|
1181
|
-
contents.push({ role: "user", parts: [{ text: payloadStr }] });
|
|
1182
|
-
const mergedLabels = { ...this.labels, ...options.labels || {} };
|
|
1183
|
-
const result = await this.genAIClient.models.generateContent({
|
|
1184
|
-
model: this.modelName,
|
|
1185
|
-
contents,
|
|
1186
|
-
config: {
|
|
1187
|
-
...this.chatConfig,
|
|
1188
|
-
...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
|
|
1189
|
-
}
|
|
1190
|
-
});
|
|
1191
|
-
this.lastResponseMetadata = {
|
|
1192
|
-
modelVersion: result.modelVersion || null,
|
|
1193
|
-
requestedModel: this.modelName,
|
|
1194
|
-
promptTokens: result.usageMetadata?.promptTokenCount || 0,
|
|
1195
|
-
responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
|
|
1196
|
-
totalTokens: result.usageMetadata?.totalTokenCount || 0,
|
|
1197
|
-
timestamp: Date.now()
|
|
1198
|
-
};
|
|
1199
|
-
this._cumulativeUsage = {
|
|
1200
|
-
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1201
|
-
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1202
|
-
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1203
|
-
attempts: 1
|
|
1204
|
-
};
|
|
1205
|
-
if (result.usageMetadata && logger_default.level !== "silent") {
|
|
1206
|
-
logger_default.debug(`Stateless message metadata: ${JSON.stringify({
|
|
1207
|
-
modelVersion: result.modelVersion || "not-provided",
|
|
1208
|
-
promptTokens: result.usageMetadata.promptTokenCount,
|
|
1209
|
-
responseTokens: result.usageMetadata.candidatesTokenCount
|
|
1210
|
-
})}`);
|
|
1211
|
-
}
|
|
1212
|
-
const modelResponse = result.text;
|
|
1213
|
-
const extractedJSON = extractJSON(modelResponse);
|
|
1214
|
-
let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
|
|
1215
|
-
if (validatorFn) {
|
|
1216
|
-
await validatorFn(transformedPayload);
|
|
1217
|
-
}
|
|
1218
|
-
return transformedPayload;
|
|
1219
|
-
}
|
|
1220
|
-
function attemptJSONRecovery(text, maxAttempts = 100) {
|
|
1221
|
-
if (!text || typeof text !== "string") return null;
|
|
1222
|
-
try {
|
|
1223
|
-
return JSON.parse(text);
|
|
1224
|
-
} catch (e) {
|
|
975
|
+
// ── History Management ───────────────────────────────────────────────────
|
|
976
|
+
/**
|
|
977
|
+
* Clears conversation history while preserving seeded examples.
|
|
978
|
+
* @returns {Promise<void>}
|
|
979
|
+
*/
|
|
980
|
+
async clearHistory() {
|
|
981
|
+
if (!this.chatSession) {
|
|
982
|
+
logger_default.warn("Cannot clear history: chat not initialized.");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const history = this.chatSession.getHistory();
|
|
986
|
+
const exampleHistory = history.slice(0, this.exampleCount || 0);
|
|
987
|
+
this.chatSession = this._createChatSession(exampleHistory);
|
|
988
|
+
this.lastResponseMetadata = null;
|
|
989
|
+
this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
|
|
990
|
+
logger_default.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
|
|
1225
991
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
992
|
+
/**
|
|
993
|
+
* Fully resets the chat session, clearing all history including examples.
|
|
994
|
+
* @returns {Promise<void>}
|
|
995
|
+
*/
|
|
996
|
+
async reset() {
|
|
997
|
+
if (this.chatSession) {
|
|
998
|
+
logger_default.debug("Resetting chat session...");
|
|
999
|
+
this.chatSession = this._createChatSession([]);
|
|
1000
|
+
this.exampleCount = 0;
|
|
1001
|
+
logger_default.debug("Chat session reset.");
|
|
1002
|
+
} else {
|
|
1003
|
+
logger_default.warn("Cannot reset: chat not yet initialized.");
|
|
1236
1004
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Updates system prompt and reinitializes the chat session.
|
|
1008
|
+
* @param {string} newPrompt - The new system prompt
|
|
1009
|
+
* @returns {Promise<void>}
|
|
1010
|
+
*/
|
|
1011
|
+
async updateSystemPrompt(newPrompt) {
|
|
1012
|
+
if (!newPrompt || typeof newPrompt !== "string") {
|
|
1013
|
+
throw new Error("System prompt must be a non-empty string");
|
|
1240
1014
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1015
|
+
this.systemPrompt = newPrompt.trim();
|
|
1016
|
+
this.chatConfig.systemInstruction = this.systemPrompt;
|
|
1017
|
+
logger_default.debug("Updating system prompt and reinitializing chat...");
|
|
1018
|
+
await this.init(true);
|
|
1019
|
+
}
|
|
1020
|
+
// ── Private Helpers ──────────────────────────────────────────────────────
|
|
1021
|
+
/**
|
|
1022
|
+
* Normalizes a payload to a string for sending.
|
|
1023
|
+
* @param {*} payload
|
|
1024
|
+
* @returns {string}
|
|
1025
|
+
* @private
|
|
1026
|
+
*/
|
|
1027
|
+
_preparePayload(payload) {
|
|
1028
|
+
if (payload && isJSON(payload)) {
|
|
1029
|
+
return JSON.stringify(payload, null, 2);
|
|
1030
|
+
} else if (typeof payload === "string") {
|
|
1031
|
+
return payload;
|
|
1032
|
+
} else if (typeof payload === "boolean" || typeof payload === "number") {
|
|
1033
|
+
return payload.toString();
|
|
1034
|
+
} else if (payload === null || payload === void 0) {
|
|
1035
|
+
return JSON.stringify({});
|
|
1036
|
+
} else {
|
|
1037
|
+
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
1244
1038
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
var transformer_default = Transformer;
|
|
1042
|
+
|
|
1043
|
+
// chat.js
|
|
1044
|
+
var Chat = class extends base_default {
|
|
1045
|
+
/**
|
|
1046
|
+
* @param {ChatOptions} [options={}]
|
|
1047
|
+
*/
|
|
1048
|
+
constructor(options = {}) {
|
|
1049
|
+
if (options.systemPrompt === void 0) {
|
|
1050
|
+
options = { ...options, systemPrompt: "You are a helpful AI assistant." };
|
|
1250
1051
|
}
|
|
1052
|
+
super(options);
|
|
1053
|
+
logger_default.debug(`Chat created with model: ${this.modelName}`);
|
|
1251
1054
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1055
|
+
/**
|
|
1056
|
+
* Send a text message and get a response. Adds to conversation history.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {string} message - The user's message
|
|
1059
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1060
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
1061
|
+
* @returns {Promise<ChatResponse>} Response with text and usage data
|
|
1062
|
+
*/
|
|
1063
|
+
async send(message, opts = {}) {
|
|
1064
|
+
if (!this.chatSession) await this.init();
|
|
1065
|
+
const mergedLabels = { ...this.labels, ...opts.labels || {} };
|
|
1066
|
+
const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
|
|
1067
|
+
const sendParams = { message };
|
|
1068
|
+
if (hasLabels) {
|
|
1069
|
+
sendParams.config = { labels: mergedLabels };
|
|
1256
1070
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1071
|
+
const result = await this.chatSession.sendMessage(sendParams);
|
|
1072
|
+
this._captureMetadata(result);
|
|
1073
|
+
this._cumulativeUsage = {
|
|
1074
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1075
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1076
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1077
|
+
attempts: 1
|
|
1078
|
+
};
|
|
1079
|
+
return {
|
|
1080
|
+
text: result.text || "",
|
|
1081
|
+
usage: this.getLastUsage()
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
var chat_default = Chat;
|
|
1086
|
+
|
|
1087
|
+
// message.js
|
|
1088
|
+
var Message = class extends base_default {
|
|
1089
|
+
/**
|
|
1090
|
+
* @param {MessageOptions} [options={}]
|
|
1091
|
+
*/
|
|
1092
|
+
constructor(options = {}) {
|
|
1093
|
+
super(options);
|
|
1094
|
+
if (options.responseSchema) {
|
|
1095
|
+
this.chatConfig.responseSchema = options.responseSchema;
|
|
1260
1096
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
brackets--;
|
|
1097
|
+
if (options.responseMimeType) {
|
|
1098
|
+
this.chatConfig.responseMimeType = options.responseMimeType;
|
|
1264
1099
|
}
|
|
1100
|
+
this._isStructured = !!(options.responseSchema || options.responseMimeType === "application/json");
|
|
1101
|
+
logger_default.debug(`Message created (structured=${this._isStructured})`);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Initialize the Message client.
|
|
1105
|
+
* Override: creates genAIClient only, NO chat session (stateless).
|
|
1106
|
+
* @param {boolean} [force=false]
|
|
1107
|
+
* @returns {Promise<void>}
|
|
1108
|
+
*/
|
|
1109
|
+
async init(force = false) {
|
|
1110
|
+
if (this._initialized && !force) return;
|
|
1111
|
+
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
1265
1112
|
try {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
1269
|
-
}
|
|
1270
|
-
return result;
|
|
1113
|
+
await this.genAIClient.models.list();
|
|
1114
|
+
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
1271
1115
|
} catch (e) {
|
|
1116
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
1272
1117
|
}
|
|
1118
|
+
this._initialized = true;
|
|
1119
|
+
logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
1273
1120
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (!inString2) {
|
|
1295
|
-
if (char === "{") braces2++;
|
|
1296
|
-
else if (char === "}") braces2--;
|
|
1297
|
-
else if (char === "[") brackets2++;
|
|
1298
|
-
else if (char === "]") brackets2--;
|
|
1121
|
+
/**
|
|
1122
|
+
* Send a stateless message and get a response.
|
|
1123
|
+
* Each call is independent — no history is maintained.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {Object|string} payload - The message or data to send
|
|
1126
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1127
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
1128
|
+
* @returns {Promise<MessageResponse>} Response with text, optional data, and usage
|
|
1129
|
+
*/
|
|
1130
|
+
async send(payload, opts = {}) {
|
|
1131
|
+
if (!this._initialized) await this.init();
|
|
1132
|
+
const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
1133
|
+
const contents = [{ role: "user", parts: [{ text: payloadStr }] }];
|
|
1134
|
+
const mergedLabels = { ...this.labels, ...opts.labels || {} };
|
|
1135
|
+
const result = await this.genAIClient.models.generateContent({
|
|
1136
|
+
model: this.modelName,
|
|
1137
|
+
contents,
|
|
1138
|
+
config: {
|
|
1139
|
+
...this.chatConfig,
|
|
1140
|
+
...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
|
|
1299
1141
|
}
|
|
1142
|
+
});
|
|
1143
|
+
this._captureMetadata(result);
|
|
1144
|
+
this._cumulativeUsage = {
|
|
1145
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1146
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1147
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1148
|
+
attempts: 1
|
|
1149
|
+
};
|
|
1150
|
+
if (result.usageMetadata && logger_default.level !== "silent") {
|
|
1151
|
+
logger_default.debug(`Message response: model=${result.modelVersion || "unknown"}, tokens=${result.usageMetadata.totalTokenCount}`);
|
|
1300
1152
|
}
|
|
1301
|
-
|
|
1153
|
+
const text = result.text || "";
|
|
1154
|
+
const response = {
|
|
1155
|
+
text,
|
|
1156
|
+
usage: this.getLastUsage()
|
|
1157
|
+
};
|
|
1158
|
+
if (this._isStructured) {
|
|
1302
1159
|
try {
|
|
1303
|
-
|
|
1304
|
-
if (logger_default.level !== "silent") {
|
|
1305
|
-
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
|
|
1306
|
-
}
|
|
1307
|
-
return result;
|
|
1308
|
-
} catch (e) {
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
if (i > 5) {
|
|
1312
|
-
let fixedText = workingText;
|
|
1313
|
-
if (inString2) {
|
|
1314
|
-
fixedText += '"';
|
|
1315
|
-
}
|
|
1316
|
-
while (braces2 > 0) {
|
|
1317
|
-
fixedText += "}";
|
|
1318
|
-
braces2--;
|
|
1319
|
-
}
|
|
1320
|
-
while (brackets2 > 0) {
|
|
1321
|
-
fixedText += "]";
|
|
1322
|
-
brackets2--;
|
|
1323
|
-
}
|
|
1324
|
-
try {
|
|
1325
|
-
const result = JSON.parse(fixedText);
|
|
1326
|
-
if (logger_default.level !== "silent") {
|
|
1327
|
-
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
1328
|
-
}
|
|
1329
|
-
return result;
|
|
1160
|
+
response.data = extractJSON(text);
|
|
1330
1161
|
} catch (e) {
|
|
1162
|
+
logger_default.warn(`Could not parse structured response: ${e.message}`);
|
|
1163
|
+
response.data = null;
|
|
1331
1164
|
}
|
|
1332
1165
|
}
|
|
1166
|
+
return response;
|
|
1333
1167
|
}
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const attempt = JSON.stringify(data);
|
|
1339
|
-
if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
|
|
1340
|
-
if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
|
|
1341
|
-
return true;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
return false;
|
|
1345
|
-
} catch (e) {
|
|
1346
|
-
return false;
|
|
1168
|
+
// ── No-ops for stateless class ──
|
|
1169
|
+
/** @returns {Array} Always returns empty array (stateless). */
|
|
1170
|
+
getHistory() {
|
|
1171
|
+
return [];
|
|
1347
1172
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
if (typeof string !== "string") return false;
|
|
1351
|
-
try {
|
|
1352
|
-
const result = JSON.parse(string);
|
|
1353
|
-
const type = Object.prototype.toString.call(result);
|
|
1354
|
-
return type === "[object Object]" || type === "[object Array]";
|
|
1355
|
-
} catch (err) {
|
|
1356
|
-
return false;
|
|
1173
|
+
/** No-op (stateless). */
|
|
1174
|
+
async clearHistory() {
|
|
1357
1175
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1176
|
+
/** Not supported on Message (stateless). */
|
|
1177
|
+
async seed() {
|
|
1178
|
+
logger_default.warn("Message is stateless \u2014 seed() has no effect. Use Transformer or Chat for few-shot learning.");
|
|
1179
|
+
return [];
|
|
1362
1180
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1181
|
+
/**
|
|
1182
|
+
* Not supported on Message (stateless).
|
|
1183
|
+
* @param {any} [_nextPayload]
|
|
1184
|
+
* @returns {Promise<{inputTokens: number}>}
|
|
1185
|
+
*/
|
|
1186
|
+
async estimate(_nextPayload) {
|
|
1187
|
+
throw new Error("Message is stateless \u2014 use estimate() on Chat or Transformer which have conversation context.");
|
|
1365
1188
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1189
|
+
};
|
|
1190
|
+
var message_default = Message;
|
|
1191
|
+
|
|
1192
|
+
// tool-agent.js
|
|
1193
|
+
var ToolAgent = class extends base_default {
|
|
1194
|
+
/**
|
|
1195
|
+
* @param {ToolAgentOptions} [options={}]
|
|
1196
|
+
*/
|
|
1197
|
+
constructor(options = {}) {
|
|
1198
|
+
if (options.systemPrompt === void 0) {
|
|
1199
|
+
options = { ...options, systemPrompt: "You are a helpful AI assistant." };
|
|
1200
|
+
}
|
|
1201
|
+
super(options);
|
|
1202
|
+
this.tools = options.tools || [];
|
|
1203
|
+
this.toolExecutor = options.toolExecutor || null;
|
|
1204
|
+
if (this.tools.length > 0 && !this.toolExecutor) {
|
|
1205
|
+
throw new Error("ToolAgent: tools provided without a toolExecutor. Provide a toolExecutor function to handle tool calls.");
|
|
1206
|
+
}
|
|
1207
|
+
if (this.toolExecutor && this.tools.length === 0) {
|
|
1208
|
+
throw new Error("ToolAgent: toolExecutor provided without tools. Provide tool declarations so the model knows what tools are available.");
|
|
1209
|
+
}
|
|
1210
|
+
this.maxToolRounds = options.maxToolRounds || 10;
|
|
1211
|
+
this.onToolCall = options.onToolCall || null;
|
|
1212
|
+
this.onBeforeExecution = options.onBeforeExecution || null;
|
|
1213
|
+
this.writeDir = options.writeDir || null;
|
|
1214
|
+
this._stopped = false;
|
|
1215
|
+
if (this.tools.length > 0) {
|
|
1216
|
+
this.chatConfig.tools = [{ functionDeclarations: this.tools }];
|
|
1217
|
+
this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
1218
|
+
}
|
|
1219
|
+
logger_default.debug(`ToolAgent created with ${this.tools.length} tools`);
|
|
1220
|
+
}
|
|
1221
|
+
// ── Non-Streaming Chat ───────────────────────────────────────────────────
|
|
1222
|
+
/**
|
|
1223
|
+
* Send a message and get a complete response (non-streaming).
|
|
1224
|
+
* Automatically handles the tool-use loop.
|
|
1225
|
+
*
|
|
1226
|
+
* @param {string} message - The user's message
|
|
1227
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1228
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
1229
|
+
* @returns {Promise<AgentResponse>} Response with text, toolCalls, and usage
|
|
1230
|
+
*/
|
|
1231
|
+
async chat(message, opts = {}) {
|
|
1232
|
+
if (!this.chatSession) await this.init();
|
|
1233
|
+
this._stopped = false;
|
|
1234
|
+
const allToolCalls = [];
|
|
1235
|
+
let response = await this.chatSession.sendMessage({ message });
|
|
1236
|
+
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
1237
|
+
if (this._stopped) break;
|
|
1238
|
+
const functionCalls = response.functionCalls;
|
|
1239
|
+
if (!functionCalls || functionCalls.length === 0) break;
|
|
1240
|
+
const toolResults = await Promise.all(
|
|
1241
|
+
functionCalls.map(async (call) => {
|
|
1242
|
+
if (this.onToolCall) {
|
|
1243
|
+
try {
|
|
1244
|
+
this.onToolCall(call.name, call.args);
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
logger_default.warn(`onToolCall callback error: ${e.message}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (this.onBeforeExecution) {
|
|
1250
|
+
try {
|
|
1251
|
+
const allowed = await this.onBeforeExecution(call.name, call.args);
|
|
1252
|
+
if (allowed === false) {
|
|
1253
|
+
const result2 = { error: "Execution denied by onBeforeExecution callback" };
|
|
1254
|
+
allToolCalls.push({ name: call.name, args: call.args, result: result2 });
|
|
1255
|
+
return { id: call.id, name: call.name, result: result2 };
|
|
1256
|
+
}
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
let result;
|
|
1262
|
+
try {
|
|
1263
|
+
result = await this.toolExecutor(call.name, call.args);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
1266
|
+
result = { error: err.message };
|
|
1267
|
+
}
|
|
1268
|
+
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
1269
|
+
return { id: call.id, name: call.name, result };
|
|
1270
|
+
})
|
|
1271
|
+
);
|
|
1272
|
+
response = await this.chatSession.sendMessage({
|
|
1273
|
+
message: toolResults.map((r) => ({
|
|
1274
|
+
functionResponse: {
|
|
1275
|
+
id: r.id,
|
|
1276
|
+
name: r.name,
|
|
1277
|
+
response: { output: r.result }
|
|
1278
|
+
}
|
|
1279
|
+
}))
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
this._captureMetadata(response);
|
|
1283
|
+
this._cumulativeUsage = {
|
|
1284
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1285
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1286
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1287
|
+
attempts: 1
|
|
1288
|
+
};
|
|
1289
|
+
return {
|
|
1290
|
+
text: response.text || "",
|
|
1291
|
+
toolCalls: allToolCalls,
|
|
1292
|
+
usage: this.getLastUsage()
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
// ── Streaming ────────────────────────────────────────────────────────────
|
|
1296
|
+
/**
|
|
1297
|
+
* Send a message and stream the response as events.
|
|
1298
|
+
* Automatically handles the tool-use loop between streamed rounds.
|
|
1299
|
+
*
|
|
1300
|
+
* Event types:
|
|
1301
|
+
* - `text` — A chunk of the agent's text response
|
|
1302
|
+
* - `tool_call` — The agent is about to call a tool
|
|
1303
|
+
* - `tool_result` — A tool finished executing
|
|
1304
|
+
* - `done` — The agent finished
|
|
1305
|
+
*
|
|
1306
|
+
* @param {string} message - The user's message
|
|
1307
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1308
|
+
* @yields {AgentStreamEvent}
|
|
1309
|
+
*/
|
|
1310
|
+
async *stream(message, opts = {}) {
|
|
1311
|
+
if (!this.chatSession) await this.init();
|
|
1312
|
+
this._stopped = false;
|
|
1313
|
+
const allToolCalls = [];
|
|
1314
|
+
let fullText = "";
|
|
1315
|
+
let streamResponse = await this.chatSession.sendMessageStream({ message });
|
|
1316
|
+
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
1317
|
+
if (this._stopped) break;
|
|
1318
|
+
let roundText = "";
|
|
1319
|
+
const functionCalls = [];
|
|
1320
|
+
for await (const chunk of streamResponse) {
|
|
1321
|
+
if (chunk.functionCalls) {
|
|
1322
|
+
functionCalls.push(...chunk.functionCalls);
|
|
1323
|
+
} else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
1324
|
+
const text = chunk.candidates[0].content.parts[0].text;
|
|
1325
|
+
roundText += text;
|
|
1326
|
+
fullText += text;
|
|
1327
|
+
yield { type: "text", text };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (functionCalls.length === 0) {
|
|
1331
|
+
yield {
|
|
1332
|
+
type: "done",
|
|
1333
|
+
fullText,
|
|
1334
|
+
usage: this.getLastUsage()
|
|
1335
|
+
};
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const toolResults = [];
|
|
1339
|
+
for (const call of functionCalls) {
|
|
1340
|
+
if (this._stopped) break;
|
|
1341
|
+
yield { type: "tool_call", toolName: call.name, args: call.args };
|
|
1342
|
+
if (this.onToolCall) {
|
|
1343
|
+
try {
|
|
1344
|
+
this.onToolCall(call.name, call.args);
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
logger_default.warn(`onToolCall callback error: ${e.message}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
let denied = false;
|
|
1350
|
+
if (this.onBeforeExecution) {
|
|
1351
|
+
try {
|
|
1352
|
+
const allowed = await this.onBeforeExecution(call.name, call.args);
|
|
1353
|
+
if (allowed === false) denied = true;
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
let result;
|
|
1359
|
+
if (denied) {
|
|
1360
|
+
result = { error: "Execution denied by onBeforeExecution callback" };
|
|
1361
|
+
} else {
|
|
1362
|
+
try {
|
|
1363
|
+
result = await this.toolExecutor(call.name, call.args);
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
1366
|
+
result = { error: err.message };
|
|
1367
|
+
}
|
|
1377
1368
|
}
|
|
1369
|
+
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
1370
|
+
yield { type: "tool_result", toolName: call.name, result };
|
|
1371
|
+
toolResults.push({ id: call.id, name: call.name, result });
|
|
1378
1372
|
}
|
|
1373
|
+
streamResponse = await this.chatSession.sendMessageStream({
|
|
1374
|
+
message: toolResults.map((r) => ({
|
|
1375
|
+
functionResponse: {
|
|
1376
|
+
id: r.id,
|
|
1377
|
+
name: r.name,
|
|
1378
|
+
response: { output: r.result }
|
|
1379
|
+
}
|
|
1380
|
+
}))
|
|
1381
|
+
});
|
|
1379
1382
|
}
|
|
1383
|
+
yield {
|
|
1384
|
+
type: "done",
|
|
1385
|
+
fullText,
|
|
1386
|
+
usage: this.getLastUsage(),
|
|
1387
|
+
warning: this._stopped ? "Agent was stopped" : "Max tool rounds reached"
|
|
1388
|
+
};
|
|
1380
1389
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1390
|
+
// ── Stop ────────────────────────────────────────────────────────────────
|
|
1391
|
+
/**
|
|
1392
|
+
* Stop the agent before the next tool execution round.
|
|
1393
|
+
* If called during a chat() or stream() loop, the agent will finish
|
|
1394
|
+
* the current round and then stop.
|
|
1395
|
+
*/
|
|
1396
|
+
stop() {
|
|
1397
|
+
this._stopped = true;
|
|
1398
|
+
logger_default.info("ToolAgent stopped");
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var tool_agent_default = ToolAgent;
|
|
1402
|
+
|
|
1403
|
+
// code-agent.js
|
|
1404
|
+
var import_node_child_process = require("node:child_process");
|
|
1405
|
+
var import_promises2 = require("node:fs/promises");
|
|
1406
|
+
var import_node_path = require("node:path");
|
|
1407
|
+
var import_node_crypto = require("node:crypto");
|
|
1408
|
+
var MAX_OUTPUT_CHARS = 5e4;
|
|
1409
|
+
var MAX_FILE_TREE_LINES = 500;
|
|
1410
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".next", "build", "__pycache__"]);
|
|
1411
|
+
var CodeAgent = class extends base_default {
|
|
1412
|
+
/**
|
|
1413
|
+
* @param {CodeAgentOptions} [options={}]
|
|
1414
|
+
*/
|
|
1415
|
+
constructor(options = {}) {
|
|
1416
|
+
if (options.systemPrompt === void 0) {
|
|
1417
|
+
options = { ...options, systemPrompt: "" };
|
|
1418
|
+
}
|
|
1419
|
+
super(options);
|
|
1420
|
+
this.workingDirectory = options.workingDirectory || process.cwd();
|
|
1421
|
+
this.maxRounds = options.maxRounds || 10;
|
|
1422
|
+
this.timeout = options.timeout || 3e4;
|
|
1423
|
+
this.onBeforeExecution = options.onBeforeExecution || null;
|
|
1424
|
+
this.onCodeExecution = options.onCodeExecution || null;
|
|
1425
|
+
this.importantFiles = options.importantFiles || [];
|
|
1426
|
+
this.writeDir = options.writeDir || (0, import_node_path.join)(this.workingDirectory, "tmp");
|
|
1427
|
+
this.keepArtifacts = options.keepArtifacts ?? false;
|
|
1428
|
+
this.comments = options.comments ?? false;
|
|
1429
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
1430
|
+
this._codebaseContext = null;
|
|
1431
|
+
this._contextGathered = false;
|
|
1432
|
+
this._stopped = false;
|
|
1433
|
+
this._activeProcess = null;
|
|
1434
|
+
this._userSystemPrompt = options.systemPrompt || "";
|
|
1435
|
+
this._allExecutions = [];
|
|
1436
|
+
this.chatConfig.tools = [{
|
|
1437
|
+
functionDeclarations: [{
|
|
1438
|
+
name: "execute_code",
|
|
1439
|
+
description: "Execute JavaScript code in a Node.js child process. The code has access to all Node.js built-in modules (fs, path, child_process, http, etc.). Use console.log() to produce output that will be returned to you. The code runs in the working directory with the same environment variables as the parent process.",
|
|
1440
|
+
parametersJsonSchema: {
|
|
1441
|
+
type: "object",
|
|
1442
|
+
properties: {
|
|
1443
|
+
code: {
|
|
1444
|
+
type: "string",
|
|
1445
|
+
description: "JavaScript code to execute. Use console.log() for output. You can import any built-in Node.js module."
|
|
1446
|
+
},
|
|
1447
|
+
purpose: {
|
|
1448
|
+
type: "string",
|
|
1449
|
+
description: 'A short 2-4 word slug describing what this script does (e.g., "read-config", "parse-logs", "fetch-api-data"). Used for naming the script file.'
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
required: ["code"]
|
|
1394
1453
|
}
|
|
1395
|
-
}
|
|
1454
|
+
}]
|
|
1455
|
+
}];
|
|
1456
|
+
this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
1457
|
+
logger_default.debug(`CodeAgent created for directory: ${this.workingDirectory}`);
|
|
1458
|
+
}
|
|
1459
|
+
// ── Init ─────────────────────────────────────────────────────────────────
|
|
1460
|
+
/**
|
|
1461
|
+
* Initialize the agent: gather codebase context, build system prompt,
|
|
1462
|
+
* and create the chat session.
|
|
1463
|
+
* @param {boolean} [force=false]
|
|
1464
|
+
*/
|
|
1465
|
+
async init(force = false) {
|
|
1466
|
+
if (this.chatSession && !force) return;
|
|
1467
|
+
if (!this._contextGathered || force) {
|
|
1468
|
+
await this._gatherCodebaseContext();
|
|
1396
1469
|
}
|
|
1470
|
+
const systemPrompt = this._buildSystemPrompt();
|
|
1471
|
+
this.chatConfig.systemInstruction = systemPrompt;
|
|
1472
|
+
await super.init(force);
|
|
1397
1473
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1474
|
+
// ── Context Gathering ────────────────────────────────────────────────────
|
|
1475
|
+
/**
|
|
1476
|
+
* Gather file tree and key file contents from the working directory.
|
|
1477
|
+
* @private
|
|
1478
|
+
*/
|
|
1479
|
+
async _gatherCodebaseContext() {
|
|
1480
|
+
let fileTree = "";
|
|
1481
|
+
try {
|
|
1482
|
+
fileTree = await this._getFileTreeGit();
|
|
1483
|
+
} catch {
|
|
1484
|
+
logger_default.debug("git ls-files failed, falling back to readdir");
|
|
1485
|
+
fileTree = await this._getFileTreeReaddir(this.workingDirectory, 0, 3);
|
|
1486
|
+
}
|
|
1487
|
+
const lines = fileTree.split("\n");
|
|
1488
|
+
if (lines.length > MAX_FILE_TREE_LINES) {
|
|
1489
|
+
const truncated = lines.slice(0, MAX_FILE_TREE_LINES).join("\n");
|
|
1490
|
+
fileTree = `${truncated}
|
|
1491
|
+
... (${lines.length - MAX_FILE_TREE_LINES} more files)`;
|
|
1492
|
+
}
|
|
1493
|
+
let npmPackages = [];
|
|
1494
|
+
try {
|
|
1495
|
+
const pkgPath = (0, import_node_path.join)(this.workingDirectory, "package.json");
|
|
1496
|
+
const pkg = JSON.parse(await (0, import_promises2.readFile)(pkgPath, "utf-8"));
|
|
1497
|
+
npmPackages = [
|
|
1498
|
+
...Object.keys(pkg.dependencies || {}),
|
|
1499
|
+
...Object.keys(pkg.devDependencies || {})
|
|
1500
|
+
];
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
const importantFileContents = [];
|
|
1504
|
+
if (this.importantFiles.length > 0) {
|
|
1505
|
+
const fileTreeLines = fileTree.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1506
|
+
for (const requested of this.importantFiles) {
|
|
1507
|
+
const resolved = this._resolveImportantFile(requested, fileTreeLines);
|
|
1508
|
+
if (!resolved) {
|
|
1509
|
+
logger_default.warn(`importantFiles: could not locate "${requested}"`);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
try {
|
|
1513
|
+
const fullPath = (0, import_node_path.join)(this.workingDirectory, resolved);
|
|
1514
|
+
const content = await (0, import_promises2.readFile)(fullPath, "utf-8");
|
|
1515
|
+
importantFileContents.push({ path: resolved, content });
|
|
1516
|
+
} catch (e) {
|
|
1517
|
+
logger_default.warn(`importantFiles: could not read "${resolved}": ${e.message}`);
|
|
1518
|
+
}
|
|
1403
1519
|
}
|
|
1404
1520
|
}
|
|
1521
|
+
this._codebaseContext = { fileTree, npmPackages, importantFileContents };
|
|
1522
|
+
this._contextGathered = true;
|
|
1405
1523
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1524
|
+
/**
|
|
1525
|
+
* Resolve an importantFiles entry against the file tree.
|
|
1526
|
+
* Supports exact matches and partial (basename/suffix) matches.
|
|
1527
|
+
* @private
|
|
1528
|
+
* @param {string} filename
|
|
1529
|
+
* @param {string[]} fileTreeLines
|
|
1530
|
+
* @returns {string|null}
|
|
1531
|
+
*/
|
|
1532
|
+
_resolveImportantFile(filename, fileTreeLines) {
|
|
1533
|
+
const exact = fileTreeLines.find((line) => line === filename);
|
|
1534
|
+
if (exact) return exact;
|
|
1535
|
+
const partial = fileTreeLines.find(
|
|
1536
|
+
(line) => line.endsWith("/" + filename) || line.endsWith(import_node_path.sep + filename)
|
|
1537
|
+
);
|
|
1538
|
+
return partial || null;
|
|
1409
1539
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1540
|
+
/**
|
|
1541
|
+
* Get file tree using git ls-files.
|
|
1542
|
+
* @private
|
|
1543
|
+
* @returns {Promise<string>}
|
|
1544
|
+
*/
|
|
1545
|
+
async _getFileTreeGit() {
|
|
1546
|
+
return new Promise((resolve2, reject) => {
|
|
1547
|
+
(0, import_node_child_process.execFile)("git", ["ls-files"], {
|
|
1548
|
+
cwd: this.workingDirectory,
|
|
1549
|
+
timeout: 5e3,
|
|
1550
|
+
maxBuffer: 5 * 1024 * 1024
|
|
1551
|
+
}, (err, stdout) => {
|
|
1552
|
+
if (err) return reject(err);
|
|
1553
|
+
resolve2(stdout.trim());
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1413
1556
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1557
|
+
/**
|
|
1558
|
+
* Fallback file tree via recursive readdir.
|
|
1559
|
+
* @private
|
|
1560
|
+
* @param {string} dir
|
|
1561
|
+
* @param {number} depth
|
|
1562
|
+
* @param {number} maxDepth
|
|
1563
|
+
* @returns {Promise<string>}
|
|
1564
|
+
*/
|
|
1565
|
+
async _getFileTreeReaddir(dir, depth, maxDepth) {
|
|
1566
|
+
if (depth >= maxDepth) return "";
|
|
1567
|
+
const entries = [];
|
|
1568
|
+
try {
|
|
1569
|
+
const items = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
|
|
1570
|
+
for (const item of items) {
|
|
1571
|
+
if (IGNORE_DIRS.has(item.name)) continue;
|
|
1572
|
+
if (item.name.startsWith(".") && depth === 0 && item.isDirectory()) continue;
|
|
1573
|
+
const relativePath = (0, import_node_path.join)(dir, item.name).replace(this.workingDirectory + "/", "");
|
|
1574
|
+
if (item.isFile()) {
|
|
1575
|
+
entries.push(relativePath);
|
|
1576
|
+
} else if (item.isDirectory()) {
|
|
1577
|
+
entries.push(relativePath + "/");
|
|
1578
|
+
const subEntries = await this._getFileTreeReaddir((0, import_node_path.join)(dir, item.name), depth + 1, maxDepth);
|
|
1579
|
+
if (subEntries) entries.push(subEntries);
|
|
1580
|
+
}
|
|
1424
1581
|
}
|
|
1582
|
+
} catch {
|
|
1425
1583
|
}
|
|
1584
|
+
return entries.join("\n");
|
|
1426
1585
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1586
|
+
/**
|
|
1587
|
+
* Build the full system prompt with codebase context.
|
|
1588
|
+
* @private
|
|
1589
|
+
* @returns {string}
|
|
1590
|
+
*/
|
|
1591
|
+
_buildSystemPrompt() {
|
|
1592
|
+
const { fileTree, npmPackages, importantFileContents } = this._codebaseContext || { fileTree: "", npmPackages: [], importantFileContents: [] };
|
|
1593
|
+
let prompt = `You are a coding agent working in ${this.workingDirectory}.
|
|
1594
|
+
|
|
1595
|
+
## Instructions
|
|
1596
|
+
- Use the execute_code tool to accomplish tasks by writing JavaScript code
|
|
1597
|
+
- Always provide a short descriptive \`purpose\` parameter (2-4 word slug like "read-config") when calling execute_code
|
|
1598
|
+
- Your code runs in a Node.js child process with access to all built-in modules
|
|
1599
|
+
- IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
|
|
1600
|
+
- import fs from 'fs';
|
|
1601
|
+
- import path from 'path';
|
|
1602
|
+
- import { execSync } from 'child_process';
|
|
1603
|
+
- Use console.log() to produce output \u2014 that's how results are returned to you
|
|
1604
|
+
- Write efficient scripts that do multiple things per execution when possible
|
|
1605
|
+
- For parallel async operations, use Promise.all():
|
|
1606
|
+
const [a, b] = await Promise.all([fetchA(), fetchB()]);
|
|
1607
|
+
- Read files with fs.readFileSync() when you need to understand their contents
|
|
1608
|
+
- Handle errors in your scripts with try/catch so you get useful error messages
|
|
1609
|
+
- Top-level await is supported
|
|
1610
|
+
- The working directory is: ${this.workingDirectory}`;
|
|
1611
|
+
if (this.comments) {
|
|
1612
|
+
prompt += `
|
|
1613
|
+
- Add a JSDoc @fileoverview comment at the top of each script explaining what it does
|
|
1614
|
+
- Add brief JSDoc @param comments for any functions you define`;
|
|
1615
|
+
} else {
|
|
1616
|
+
prompt += `
|
|
1617
|
+
- Do NOT write any comments in your code \u2014 save tokens. The code should be self-explanatory.`;
|
|
1440
1618
|
}
|
|
1441
|
-
if (
|
|
1442
|
-
|
|
1443
|
-
|
|
1619
|
+
if (fileTree) {
|
|
1620
|
+
prompt += `
|
|
1621
|
+
|
|
1622
|
+
## File Tree
|
|
1623
|
+
\`\`\`
|
|
1624
|
+
${fileTree}
|
|
1625
|
+
\`\`\``;
|
|
1444
1626
|
}
|
|
1445
|
-
if (
|
|
1446
|
-
|
|
1447
|
-
|
|
1627
|
+
if (npmPackages.length > 0) {
|
|
1628
|
+
prompt += `
|
|
1629
|
+
|
|
1630
|
+
## Available Packages
|
|
1631
|
+
These npm packages are installed and can be imported: ${npmPackages.join(", ")}`;
|
|
1448
1632
|
}
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1633
|
+
if (importantFileContents && importantFileContents.length > 0) {
|
|
1634
|
+
prompt += `
|
|
1635
|
+
|
|
1636
|
+
## Key Files`;
|
|
1637
|
+
for (const { path: filePath, content } of importantFileContents) {
|
|
1638
|
+
prompt += `
|
|
1639
|
+
|
|
1640
|
+
### ${filePath}
|
|
1641
|
+
\`\`\`javascript
|
|
1642
|
+
${content}
|
|
1643
|
+
\`\`\``;
|
|
1457
1644
|
}
|
|
1458
1645
|
}
|
|
1646
|
+
if (this._userSystemPrompt) {
|
|
1647
|
+
prompt += `
|
|
1648
|
+
|
|
1649
|
+
## Additional Instructions
|
|
1650
|
+
${this._userSystemPrompt}`;
|
|
1651
|
+
}
|
|
1652
|
+
return prompt;
|
|
1459
1653
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1654
|
+
// ── Code Execution ───────────────────────────────────────────────────────
|
|
1655
|
+
/**
|
|
1656
|
+
* Generate a sanitized slug from a purpose string.
|
|
1657
|
+
* @private
|
|
1658
|
+
* @param {string} [purpose]
|
|
1659
|
+
* @returns {string}
|
|
1660
|
+
*/
|
|
1661
|
+
_slugify(purpose) {
|
|
1662
|
+
if (!purpose) return (0, import_node_crypto.randomUUID)().slice(0, 8);
|
|
1663
|
+
return purpose.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Execute a JavaScript code string in a child process.
|
|
1667
|
+
* @private
|
|
1668
|
+
* @param {string} code - JavaScript code to execute
|
|
1669
|
+
* @param {string} [purpose] - Short description for file naming
|
|
1670
|
+
* @returns {Promise<{stdout: string, stderr: string, exitCode: number, denied?: boolean}>}
|
|
1671
|
+
*/
|
|
1672
|
+
async _executeCode(code, purpose) {
|
|
1673
|
+
if (this._stopped) {
|
|
1674
|
+
return { stdout: "", stderr: "Agent was stopped", exitCode: -1 };
|
|
1675
|
+
}
|
|
1676
|
+
if (this.onBeforeExecution) {
|
|
1677
|
+
try {
|
|
1678
|
+
const allowed = await this.onBeforeExecution(code);
|
|
1679
|
+
if (allowed === false) {
|
|
1680
|
+
return { stdout: "", stderr: "Execution denied by onBeforeExecution callback", exitCode: -1, denied: true };
|
|
1681
|
+
}
|
|
1682
|
+
} catch (e) {
|
|
1683
|
+
logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
await (0, import_promises2.mkdir)(this.writeDir, { recursive: true });
|
|
1687
|
+
const slug = this._slugify(purpose);
|
|
1688
|
+
const tempFile = (0, import_node_path.join)(this.writeDir, `agent-${slug}-${Date.now()}.mjs`);
|
|
1465
1689
|
try {
|
|
1466
|
-
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1690
|
+
await (0, import_promises2.writeFile)(tempFile, code, "utf-8");
|
|
1691
|
+
const result = await new Promise((resolve2) => {
|
|
1692
|
+
const child = (0, import_node_child_process.execFile)("node", [tempFile], {
|
|
1693
|
+
cwd: this.workingDirectory,
|
|
1694
|
+
timeout: this.timeout,
|
|
1695
|
+
env: process.env,
|
|
1696
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1697
|
+
}, (err, stdout, stderr) => {
|
|
1698
|
+
this._activeProcess = null;
|
|
1699
|
+
if (err) {
|
|
1700
|
+
resolve2({
|
|
1701
|
+
stdout: err.stdout || stdout || "",
|
|
1702
|
+
stderr: (err.stderr || stderr || "") + (err.killed ? "\n[EXECUTION TIMED OUT]" : ""),
|
|
1703
|
+
exitCode: err.code || 1
|
|
1704
|
+
});
|
|
1705
|
+
} else {
|
|
1706
|
+
resolve2({ stdout: stdout || "", stderr: stderr || "", exitCode: 0 });
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
this._activeProcess = child;
|
|
1476
1710
|
});
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
},
|
|
1483
|
-
{
|
|
1484
|
-
INPUT: { "name": "Bob" },
|
|
1485
|
-
OUTPUT: { "name": "Bob", "profession": "product manager", "life_as_told_by_emoji": ["\u{1F4CB}", "\u{1F91D}", "\u{1F680}", "\u{1F4AC}", "\u{1F3AF}"] }
|
|
1486
|
-
},
|
|
1487
|
-
{
|
|
1488
|
-
INPUT: { "name": "Eve" },
|
|
1489
|
-
OUTPUT: { "name": "Even", "profession": "security analyst", "life_as_told_by_emoji": ["\u{1F575}\uFE0F\u200D\u2640\uFE0F", "\u{1F512}", "\u{1F4BB}", "\u{1F440}", "\u26A1\uFE0F"] }
|
|
1711
|
+
const totalLen = result.stdout.length + result.stderr.length;
|
|
1712
|
+
if (totalLen > MAX_OUTPUT_CHARS) {
|
|
1713
|
+
const half = Math.floor(MAX_OUTPUT_CHARS / 2);
|
|
1714
|
+
if (result.stdout.length > half) {
|
|
1715
|
+
result.stdout = result.stdout.slice(0, half) + "\n...[OUTPUT TRUNCATED]";
|
|
1490
1716
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
await transformer.seed(examples);
|
|
1494
|
-
logger_default.info("AI Transformer initialized and seeded with examples.");
|
|
1495
|
-
const normalResponse = await transformer.message({ "name": "AK" });
|
|
1496
|
-
logger_default.info(`Normal Payload Transformed: ${JSON.stringify(normalResponse)}`);
|
|
1497
|
-
const mockValidator = async (payload) => {
|
|
1498
|
-
if (!payload.profession || !payload.life_as_told_by_emoji) {
|
|
1499
|
-
throw new Error("Missing required fields: profession or life_as_told_by_emoji");
|
|
1717
|
+
if (result.stderr.length > half) {
|
|
1718
|
+
result.stderr = result.stderr.slice(0, half) + "\n...[STDERR TRUNCATED]";
|
|
1500
1719
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1720
|
+
}
|
|
1721
|
+
this._allExecutions.push({
|
|
1722
|
+
code,
|
|
1723
|
+
purpose: purpose || null,
|
|
1724
|
+
output: result.stdout,
|
|
1725
|
+
stderr: result.stderr,
|
|
1726
|
+
exitCode: result.exitCode,
|
|
1727
|
+
filePath: this.keepArtifacts ? tempFile : null
|
|
1728
|
+
});
|
|
1729
|
+
if (this.onCodeExecution) {
|
|
1730
|
+
try {
|
|
1731
|
+
this.onCodeExecution(code, result);
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
logger_default.warn(`onCodeExecution callback error: ${e.message}`);
|
|
1503
1734
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
{
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
} catch (error) {
|
|
1514
|
-
logger_default.error(`Error in AI Transformer script: ${error?.message || error}`);
|
|
1515
|
-
if (NODE_ENV2 === "dev") debugger;
|
|
1735
|
+
}
|
|
1736
|
+
return result;
|
|
1737
|
+
} finally {
|
|
1738
|
+
if (!this.keepArtifacts) {
|
|
1739
|
+
try {
|
|
1740
|
+
await (0, import_promises2.unlink)(tempFile);
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1516
1744
|
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Format execution result as a string for the model.
|
|
1748
|
+
* @private
|
|
1749
|
+
* @param {{stdout: string, stderr: string, exitCode: number}} result
|
|
1750
|
+
* @returns {string}
|
|
1751
|
+
*/
|
|
1752
|
+
_formatOutput(result) {
|
|
1753
|
+
let output = "";
|
|
1754
|
+
if (result.stdout) output += result.stdout;
|
|
1755
|
+
if (result.stderr) output += (output ? "\n" : "") + `[STDERR]: ${result.stderr}`;
|
|
1756
|
+
if (result.exitCode !== 0) output += (output ? "\n" : "") + `[EXIT CODE]: ${result.exitCode}`;
|
|
1757
|
+
return output || "(no output)";
|
|
1758
|
+
}
|
|
1759
|
+
// ── Non-Streaming Chat ───────────────────────────────────────────────────
|
|
1760
|
+
/**
|
|
1761
|
+
* Send a message and get a complete response (non-streaming).
|
|
1762
|
+
* Automatically handles the code execution loop.
|
|
1763
|
+
*
|
|
1764
|
+
* @param {string} message - The user's message
|
|
1765
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1766
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
1767
|
+
* @returns {Promise<CodeAgentResponse>} Response with text, codeExecutions, and usage
|
|
1768
|
+
*/
|
|
1769
|
+
async chat(message, opts = {}) {
|
|
1770
|
+
if (!this.chatSession) await this.init();
|
|
1771
|
+
this._stopped = false;
|
|
1772
|
+
const codeExecutions = [];
|
|
1773
|
+
let consecutiveFailures = 0;
|
|
1774
|
+
let response = await this.chatSession.sendMessage({ message });
|
|
1775
|
+
for (let round = 0; round < this.maxRounds; round++) {
|
|
1776
|
+
if (this._stopped) break;
|
|
1777
|
+
const functionCalls = response.functionCalls;
|
|
1778
|
+
if (!functionCalls || functionCalls.length === 0) break;
|
|
1779
|
+
const results = [];
|
|
1780
|
+
for (const call of functionCalls) {
|
|
1781
|
+
if (this._stopped) break;
|
|
1782
|
+
const code = call.args?.code || "";
|
|
1783
|
+
const purpose = call.args?.purpose;
|
|
1784
|
+
const result = await this._executeCode(code, purpose);
|
|
1785
|
+
codeExecutions.push({
|
|
1786
|
+
code,
|
|
1787
|
+
purpose: this._slugify(purpose),
|
|
1788
|
+
output: result.stdout,
|
|
1789
|
+
stderr: result.stderr,
|
|
1790
|
+
exitCode: result.exitCode
|
|
1791
|
+
});
|
|
1792
|
+
if (result.exitCode !== 0 && !result.denied) {
|
|
1793
|
+
consecutiveFailures++;
|
|
1794
|
+
} else {
|
|
1795
|
+
consecutiveFailures = 0;
|
|
1796
|
+
}
|
|
1797
|
+
let output = this._formatOutput(result);
|
|
1798
|
+
if (consecutiveFailures >= this.maxRetries) {
|
|
1799
|
+
output += `
|
|
1800
|
+
|
|
1801
|
+
[RETRY LIMIT REACHED] You have failed ${this.maxRetries} consecutive attempts. STOP trying to execute code. Instead, respond with: 1) What you were trying to do, 2) The errors you encountered, 3) Questions for the user about how to resolve it.`;
|
|
1802
|
+
}
|
|
1803
|
+
results.push({
|
|
1804
|
+
id: call.id,
|
|
1805
|
+
name: call.name,
|
|
1806
|
+
result: output
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
if (this._stopped) break;
|
|
1810
|
+
response = await this.chatSession.sendMessage({
|
|
1811
|
+
message: results.map((r) => ({
|
|
1812
|
+
functionResponse: {
|
|
1813
|
+
id: r.id,
|
|
1814
|
+
name: r.name,
|
|
1815
|
+
response: { output: r.result }
|
|
1816
|
+
}
|
|
1817
|
+
}))
|
|
1818
|
+
});
|
|
1819
|
+
if (consecutiveFailures >= this.maxRetries) break;
|
|
1820
|
+
}
|
|
1821
|
+
this._captureMetadata(response);
|
|
1822
|
+
this._cumulativeUsage = {
|
|
1823
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1824
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1825
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1826
|
+
attempts: 1
|
|
1827
|
+
};
|
|
1828
|
+
return {
|
|
1829
|
+
text: response.text || "",
|
|
1830
|
+
codeExecutions,
|
|
1831
|
+
usage: this.getLastUsage()
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
// ── Streaming ────────────────────────────────────────────────────────────
|
|
1835
|
+
/**
|
|
1836
|
+
* Send a message and stream the response as events.
|
|
1837
|
+
* Automatically handles the code execution loop between streamed rounds.
|
|
1838
|
+
*
|
|
1839
|
+
* Event types:
|
|
1840
|
+
* - `text` — A chunk of the agent's text response
|
|
1841
|
+
* - `code` — The agent is about to execute code
|
|
1842
|
+
* - `output` — Code finished executing
|
|
1843
|
+
* - `done` — The agent finished
|
|
1844
|
+
*
|
|
1845
|
+
* @param {string} message - The user's message
|
|
1846
|
+
* @param {Object} [opts={}] - Per-message options
|
|
1847
|
+
* @yields {CodeAgentStreamEvent}
|
|
1848
|
+
*/
|
|
1849
|
+
async *stream(message, opts = {}) {
|
|
1850
|
+
if (!this.chatSession) await this.init();
|
|
1851
|
+
this._stopped = false;
|
|
1852
|
+
const codeExecutions = [];
|
|
1853
|
+
let fullText = "";
|
|
1854
|
+
let consecutiveFailures = 0;
|
|
1855
|
+
let streamResponse = await this.chatSession.sendMessageStream({ message });
|
|
1856
|
+
for (let round = 0; round < this.maxRounds; round++) {
|
|
1857
|
+
if (this._stopped) break;
|
|
1858
|
+
const functionCalls = [];
|
|
1859
|
+
for await (const chunk of streamResponse) {
|
|
1860
|
+
if (chunk.functionCalls) {
|
|
1861
|
+
functionCalls.push(...chunk.functionCalls);
|
|
1862
|
+
} else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
1863
|
+
const text = chunk.candidates[0].content.parts[0].text;
|
|
1864
|
+
fullText += text;
|
|
1865
|
+
yield { type: "text", text };
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (functionCalls.length === 0) {
|
|
1869
|
+
yield {
|
|
1870
|
+
type: "done",
|
|
1871
|
+
fullText,
|
|
1872
|
+
codeExecutions,
|
|
1873
|
+
usage: this.getLastUsage()
|
|
1874
|
+
};
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const results = [];
|
|
1878
|
+
for (const call of functionCalls) {
|
|
1879
|
+
if (this._stopped) break;
|
|
1880
|
+
const code = call.args?.code || "";
|
|
1881
|
+
const purpose = call.args?.purpose;
|
|
1882
|
+
yield { type: "code", code };
|
|
1883
|
+
const result = await this._executeCode(code, purpose);
|
|
1884
|
+
codeExecutions.push({
|
|
1885
|
+
code,
|
|
1886
|
+
purpose: this._slugify(purpose),
|
|
1887
|
+
output: result.stdout,
|
|
1888
|
+
stderr: result.stderr,
|
|
1889
|
+
exitCode: result.exitCode
|
|
1890
|
+
});
|
|
1891
|
+
yield {
|
|
1892
|
+
type: "output",
|
|
1893
|
+
code,
|
|
1894
|
+
stdout: result.stdout,
|
|
1895
|
+
stderr: result.stderr,
|
|
1896
|
+
exitCode: result.exitCode
|
|
1897
|
+
};
|
|
1898
|
+
if (result.exitCode !== 0 && !result.denied) {
|
|
1899
|
+
consecutiveFailures++;
|
|
1900
|
+
} else {
|
|
1901
|
+
consecutiveFailures = 0;
|
|
1902
|
+
}
|
|
1903
|
+
let output = this._formatOutput(result);
|
|
1904
|
+
if (consecutiveFailures >= this.maxRetries) {
|
|
1905
|
+
output += `
|
|
1906
|
+
|
|
1907
|
+
[RETRY LIMIT REACHED] You have failed ${this.maxRetries} consecutive attempts. STOP trying to execute code. Instead, respond with: 1) What you were trying to do, 2) The errors you encountered, 3) Questions for the user about how to resolve it.`;
|
|
1908
|
+
}
|
|
1909
|
+
results.push({
|
|
1910
|
+
id: call.id,
|
|
1911
|
+
name: call.name,
|
|
1912
|
+
result: output
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
if (this._stopped) break;
|
|
1916
|
+
streamResponse = await this.chatSession.sendMessageStream({
|
|
1917
|
+
message: results.map((r) => ({
|
|
1918
|
+
functionResponse: {
|
|
1919
|
+
id: r.id,
|
|
1920
|
+
name: r.name,
|
|
1921
|
+
response: { output: r.result }
|
|
1922
|
+
}
|
|
1923
|
+
}))
|
|
1924
|
+
});
|
|
1925
|
+
if (consecutiveFailures >= this.maxRetries) break;
|
|
1926
|
+
}
|
|
1927
|
+
let warning = "Max tool rounds reached";
|
|
1928
|
+
if (this._stopped) warning = "Agent was stopped";
|
|
1929
|
+
else if (consecutiveFailures >= this.maxRetries) warning = "Retry limit reached";
|
|
1930
|
+
yield {
|
|
1931
|
+
type: "done",
|
|
1932
|
+
fullText,
|
|
1933
|
+
codeExecutions,
|
|
1934
|
+
usage: this.getLastUsage(),
|
|
1935
|
+
warning
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
// ── Dump ─────────────────────────────────────────────────────────────────
|
|
1939
|
+
/**
|
|
1940
|
+
* Returns all code scripts the agent has written across all chat/stream calls.
|
|
1941
|
+
* @returns {Array<{fileName: string, script: string}>}
|
|
1942
|
+
*/
|
|
1943
|
+
dump() {
|
|
1944
|
+
return this._allExecutions.map((exec, i) => ({
|
|
1945
|
+
fileName: exec.purpose ? `agent-${exec.purpose}.mjs` : `script-${i + 1}.mjs`,
|
|
1946
|
+
purpose: exec.purpose || null,
|
|
1947
|
+
script: exec.code,
|
|
1948
|
+
filePath: exec.filePath || null
|
|
1949
|
+
}));
|
|
1950
|
+
}
|
|
1951
|
+
// ── Stop ─────────────────────────────────────────────────────────────────
|
|
1952
|
+
/**
|
|
1953
|
+
* Stop the agent before the next code execution.
|
|
1954
|
+
* If a child process is currently running, it will be killed.
|
|
1955
|
+
*/
|
|
1956
|
+
stop() {
|
|
1957
|
+
this._stopped = true;
|
|
1958
|
+
if (this._activeProcess) {
|
|
1959
|
+
try {
|
|
1960
|
+
this._activeProcess.kill("SIGTERM");
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
logger_default.info("CodeAgent stopped");
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
var code_agent_default = CodeAgent;
|
|
1968
|
+
|
|
1969
|
+
// rag-agent.js
|
|
1970
|
+
var import_node_path2 = require("node:path");
|
|
1971
|
+
var import_promises3 = require("node:fs/promises");
|
|
1972
|
+
var MIME_TYPES = {
|
|
1973
|
+
// Text
|
|
1974
|
+
".txt": "text/plain",
|
|
1975
|
+
".md": "text/plain",
|
|
1976
|
+
".csv": "text/csv",
|
|
1977
|
+
".html": "text/html",
|
|
1978
|
+
".htm": "text/html",
|
|
1979
|
+
".xml": "text/xml",
|
|
1980
|
+
".json": "application/json",
|
|
1981
|
+
".js": "text/javascript",
|
|
1982
|
+
".mjs": "text/javascript",
|
|
1983
|
+
".ts": "text/plain",
|
|
1984
|
+
".css": "text/css",
|
|
1985
|
+
".yaml": "text/plain",
|
|
1986
|
+
".yml": "text/plain",
|
|
1987
|
+
".py": "text/x-python",
|
|
1988
|
+
".rb": "text/plain",
|
|
1989
|
+
".sh": "text/plain",
|
|
1990
|
+
// Documents
|
|
1991
|
+
".pdf": "application/pdf",
|
|
1992
|
+
".doc": "application/msword",
|
|
1993
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1994
|
+
// Images
|
|
1995
|
+
".png": "image/png",
|
|
1996
|
+
".jpg": "image/jpeg",
|
|
1997
|
+
".jpeg": "image/jpeg",
|
|
1998
|
+
".gif": "image/gif",
|
|
1999
|
+
".webp": "image/webp",
|
|
2000
|
+
".svg": "image/svg+xml",
|
|
2001
|
+
// Audio
|
|
2002
|
+
".mp3": "audio/mpeg",
|
|
2003
|
+
".wav": "audio/wav",
|
|
2004
|
+
".ogg": "audio/ogg",
|
|
2005
|
+
".flac": "audio/flac",
|
|
2006
|
+
".aac": "audio/aac",
|
|
2007
|
+
// Video
|
|
2008
|
+
".mp4": "video/mp4",
|
|
2009
|
+
".webm": "video/webm",
|
|
2010
|
+
".avi": "video/x-msvideo",
|
|
2011
|
+
".mov": "video/quicktime",
|
|
2012
|
+
".mkv": "video/x-matroska"
|
|
2013
|
+
};
|
|
2014
|
+
var DEFAULT_SYSTEM_PROMPT = "You are a helpful AI assistant. Answer questions based on the provided documents and data. When referencing information, mention which document or data source it comes from.";
|
|
2015
|
+
var FILE_POLL_INTERVAL_MS = 2e3;
|
|
2016
|
+
var FILE_POLL_TIMEOUT_MS = 6e4;
|
|
2017
|
+
var RagAgent = class extends base_default {
|
|
2018
|
+
/**
|
|
2019
|
+
* @param {RagAgentOptions} [options={}]
|
|
2020
|
+
*/
|
|
2021
|
+
constructor(options = {}) {
|
|
2022
|
+
if (options.systemPrompt === void 0) {
|
|
2023
|
+
options = { ...options, systemPrompt: DEFAULT_SYSTEM_PROMPT };
|
|
2024
|
+
}
|
|
2025
|
+
super(options);
|
|
2026
|
+
this.remoteFiles = options.remoteFiles || [];
|
|
2027
|
+
this.localFiles = options.localFiles || [];
|
|
2028
|
+
this.localData = options.localData || [];
|
|
2029
|
+
this._uploadedRemoteFiles = [];
|
|
2030
|
+
this._localFileContents = [];
|
|
2031
|
+
this._initialized = false;
|
|
2032
|
+
const total = this.remoteFiles.length + this.localFiles.length + this.localData.length;
|
|
2033
|
+
logger_default.debug(`RagAgent created with ${total} context sources`);
|
|
2034
|
+
}
|
|
2035
|
+
// ── Initialization ───────────────────────────────────────────────────────
|
|
2036
|
+
/**
|
|
2037
|
+
* Uploads remote files, reads local files, and seeds all context into the chat.
|
|
2038
|
+
* @param {boolean} [force=false]
|
|
2039
|
+
* @returns {Promise<void>}
|
|
2040
|
+
*/
|
|
2041
|
+
async init(force = false) {
|
|
2042
|
+
if (this._initialized && !force) return;
|
|
2043
|
+
this._uploadedRemoteFiles = [];
|
|
2044
|
+
for (const filePath of this.remoteFiles) {
|
|
2045
|
+
const resolvedPath = (0, import_node_path2.resolve)(filePath);
|
|
2046
|
+
logger_default.debug(`Uploading remote file: ${resolvedPath}`);
|
|
2047
|
+
const ext = (0, import_node_path2.extname)(resolvedPath).toLowerCase();
|
|
2048
|
+
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2049
|
+
const uploaded = await this.genAIClient.files.upload({
|
|
2050
|
+
file: resolvedPath,
|
|
2051
|
+
config: { displayName: (0, import_node_path2.basename)(resolvedPath), mimeType }
|
|
2052
|
+
});
|
|
2053
|
+
await this._waitForFileActive(uploaded);
|
|
2054
|
+
this._uploadedRemoteFiles.push({
|
|
2055
|
+
...uploaded,
|
|
2056
|
+
originalPath: resolvedPath
|
|
2057
|
+
});
|
|
2058
|
+
logger_default.debug(`File uploaded: ${uploaded.displayName} (${uploaded.mimeType})`);
|
|
2059
|
+
}
|
|
2060
|
+
this._localFileContents = [];
|
|
2061
|
+
for (const filePath of this.localFiles) {
|
|
2062
|
+
const resolvedPath = (0, import_node_path2.resolve)(filePath);
|
|
2063
|
+
logger_default.debug(`Reading local file: ${resolvedPath}`);
|
|
2064
|
+
const content = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
|
|
2065
|
+
this._localFileContents.push({
|
|
2066
|
+
name: (0, import_node_path2.basename)(resolvedPath),
|
|
2067
|
+
content,
|
|
2068
|
+
path: resolvedPath
|
|
2069
|
+
});
|
|
2070
|
+
logger_default.debug(`Local file read: ${(0, import_node_path2.basename)(resolvedPath)} (${content.length} chars)`);
|
|
2071
|
+
}
|
|
2072
|
+
this.chatConfig.systemInstruction = /** @type {string} */
|
|
2073
|
+
this.systemPrompt;
|
|
2074
|
+
await super.init(force);
|
|
2075
|
+
const parts = [];
|
|
2076
|
+
for (const f of this._uploadedRemoteFiles) {
|
|
2077
|
+
parts.push({ fileData: { fileUri: f.uri, mimeType: f.mimeType } });
|
|
2078
|
+
}
|
|
2079
|
+
for (const lf of this._localFileContents) {
|
|
2080
|
+
parts.push({ text: `--- File: ${lf.name} ---
|
|
2081
|
+
${lf.content}` });
|
|
2082
|
+
}
|
|
2083
|
+
for (const ld of this.localData) {
|
|
2084
|
+
const serialized = typeof ld.data === "string" ? ld.data : JSON.stringify(ld.data, null, 2);
|
|
2085
|
+
parts.push({ text: `--- Data: ${ld.name} ---
|
|
2086
|
+
${serialized}` });
|
|
2087
|
+
}
|
|
2088
|
+
if (parts.length > 0) {
|
|
2089
|
+
parts.push({ text: "Here are the documents and data to analyze." });
|
|
2090
|
+
const history = [
|
|
2091
|
+
{ role: "user", parts },
|
|
2092
|
+
{ role: "model", parts: [{ text: "I have reviewed all the provided documents and data. I am ready to answer your questions about them." }] }
|
|
2093
|
+
];
|
|
2094
|
+
this.chatSession = this._createChatSession(history);
|
|
2095
|
+
}
|
|
2096
|
+
this._initialized = true;
|
|
2097
|
+
logger_default.debug(`RagAgent initialized with ${this._uploadedRemoteFiles.length} remote files, ${this._localFileContents.length} local files, ${this.localData.length} data entries`);
|
|
2098
|
+
}
|
|
2099
|
+
// ── Non-Streaming Chat ───────────────────────────────────────────────────
|
|
2100
|
+
/**
|
|
2101
|
+
* Send a message and get a complete response grounded in the loaded context.
|
|
2102
|
+
*
|
|
2103
|
+
* @param {string} message - The user's question
|
|
2104
|
+
* @param {Object} [opts={}] - Per-message options
|
|
2105
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
2106
|
+
* @returns {Promise<RagResponse>}
|
|
2107
|
+
*/
|
|
2108
|
+
async chat(message, opts = {}) {
|
|
2109
|
+
if (!this._initialized) await this.init();
|
|
2110
|
+
const response = await this.chatSession.sendMessage({ message });
|
|
2111
|
+
this._captureMetadata(response);
|
|
2112
|
+
this._cumulativeUsage = {
|
|
2113
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
2114
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
2115
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
2116
|
+
attempts: 1
|
|
2117
|
+
};
|
|
2118
|
+
return {
|
|
2119
|
+
text: response.text || "",
|
|
2120
|
+
usage: this.getLastUsage()
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
// ── Streaming ────────────────────────────────────────────────────────────
|
|
2124
|
+
/**
|
|
2125
|
+
* Send a message and stream the response as events.
|
|
2126
|
+
*
|
|
2127
|
+
* @param {string} message - The user's question
|
|
2128
|
+
* @param {Object} [opts={}] - Per-message options
|
|
2129
|
+
* @yields {RagStreamEvent}
|
|
2130
|
+
*/
|
|
2131
|
+
async *stream(message, opts = {}) {
|
|
2132
|
+
if (!this._initialized) await this.init();
|
|
2133
|
+
let fullText = "";
|
|
2134
|
+
const streamResponse = await this.chatSession.sendMessageStream({ message });
|
|
2135
|
+
for await (const chunk of streamResponse) {
|
|
2136
|
+
if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
2137
|
+
const text = chunk.candidates[0].content.parts[0].text;
|
|
2138
|
+
fullText += text;
|
|
2139
|
+
yield { type: "text", text };
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
yield {
|
|
2143
|
+
type: "done",
|
|
2144
|
+
fullText,
|
|
2145
|
+
usage: this.getLastUsage()
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
// ── Context Management ──────────────────────────────────────────────────
|
|
2149
|
+
/**
|
|
2150
|
+
* Add remote files (uploaded via Files API). Triggers reinitialize.
|
|
2151
|
+
* @param {string[]} paths
|
|
2152
|
+
* @returns {Promise<void>}
|
|
2153
|
+
*/
|
|
2154
|
+
async addRemoteFiles(paths) {
|
|
2155
|
+
this.remoteFiles.push(...paths);
|
|
2156
|
+
await this.init(true);
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Add local text files (read from disk). Triggers reinitialize.
|
|
2160
|
+
* @param {string[]} paths
|
|
2161
|
+
* @returns {Promise<void>}
|
|
2162
|
+
*/
|
|
2163
|
+
async addLocalFiles(paths) {
|
|
2164
|
+
this.localFiles.push(...paths);
|
|
2165
|
+
await this.init(true);
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Add in-memory data entries. Triggers reinitialize.
|
|
2169
|
+
* @param {LocalDataEntry[]} entries
|
|
2170
|
+
* @returns {Promise<void>}
|
|
2171
|
+
*/
|
|
2172
|
+
async addLocalData(entries) {
|
|
2173
|
+
this.localData.push(...entries);
|
|
2174
|
+
await this.init(true);
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Returns metadata about all context sources.
|
|
2178
|
+
* @returns {{ remoteFiles: Array<Object>, localFiles: Array<Object>, localData: Array<Object> }}
|
|
2179
|
+
*/
|
|
2180
|
+
getContext() {
|
|
2181
|
+
return {
|
|
2182
|
+
remoteFiles: this._uploadedRemoteFiles.map((f) => ({
|
|
2183
|
+
name: f.name,
|
|
2184
|
+
displayName: f.displayName,
|
|
2185
|
+
mimeType: f.mimeType,
|
|
2186
|
+
sizeBytes: f.sizeBytes,
|
|
2187
|
+
uri: f.uri,
|
|
2188
|
+
originalPath: f.originalPath
|
|
2189
|
+
})),
|
|
2190
|
+
localFiles: this._localFileContents.map((lf) => ({
|
|
2191
|
+
name: lf.name,
|
|
2192
|
+
path: lf.path,
|
|
2193
|
+
size: lf.content.length
|
|
2194
|
+
})),
|
|
2195
|
+
localData: this.localData.map((ld) => ({
|
|
2196
|
+
name: ld.name,
|
|
2197
|
+
type: typeof ld.data === "object" && ld.data !== null ? Array.isArray(ld.data) ? "array" : "object" : typeof ld.data
|
|
2198
|
+
}))
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
// ── Private Helpers ──────────────────────────────────────────────────────
|
|
2202
|
+
/**
|
|
2203
|
+
* Polls until an uploaded file reaches ACTIVE state.
|
|
2204
|
+
* @param {Object} file - The uploaded file object
|
|
2205
|
+
* @returns {Promise<void>}
|
|
2206
|
+
* @private
|
|
2207
|
+
*/
|
|
2208
|
+
async _waitForFileActive(file) {
|
|
2209
|
+
if (file.state === "ACTIVE") return;
|
|
2210
|
+
const start = Date.now();
|
|
2211
|
+
while (Date.now() - start < FILE_POLL_TIMEOUT_MS) {
|
|
2212
|
+
const updated = await this.genAIClient.files.get({ name: file.name });
|
|
2213
|
+
if (updated.state === "ACTIVE") return;
|
|
2214
|
+
if (updated.state === "FAILED") {
|
|
2215
|
+
throw new Error(`File processing failed: ${file.displayName || file.name}`);
|
|
2216
|
+
}
|
|
2217
|
+
await new Promise((r) => setTimeout(r, FILE_POLL_INTERVAL_MS));
|
|
2218
|
+
}
|
|
2219
|
+
throw new Error(`File processing timed out after ${FILE_POLL_TIMEOUT_MS / 1e3}s: ${file.displayName || file.name}`);
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
var rag_agent_default = RagAgent;
|
|
2223
|
+
|
|
2224
|
+
// index.js
|
|
2225
|
+
var import_genai2 = require("@google/genai");
|
|
2226
|
+
var index_default = { Transformer: transformer_default, Chat: chat_default, Message: message_default, ToolAgent: tool_agent_default, CodeAgent: code_agent_default, RagAgent: rag_agent_default };
|
|
1519
2227
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1520
2228
|
0 && (module.exports = {
|
|
1521
|
-
|
|
2229
|
+
BaseGemini,
|
|
2230
|
+
Chat,
|
|
2231
|
+
CodeAgent,
|
|
1522
2232
|
HarmBlockThreshold,
|
|
1523
2233
|
HarmCategory,
|
|
2234
|
+
Message,
|
|
2235
|
+
RagAgent,
|
|
1524
2236
|
ThinkingLevel,
|
|
2237
|
+
ToolAgent,
|
|
2238
|
+
Transformer,
|
|
1525
2239
|
attemptJSONRecovery,
|
|
2240
|
+
extractJSON,
|
|
1526
2241
|
log
|
|
1527
2242
|
});
|