ak-gemini 1.0.7 → 1.0.9
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/index.cjs +216 -16
- package/index.js +308 -19
- package/package.json +2 -1
- package/types.d.ts +13 -5
package/index.cjs
CHANGED
|
@@ -29,7 +29,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
// index.js
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
+
HarmBlockThreshold: () => import_genai.HarmBlockThreshold,
|
|
33
|
+
HarmCategory: () => import_genai.HarmCategory,
|
|
32
34
|
ThinkingLevel: () => import_genai.ThinkingLevel,
|
|
35
|
+
attemptJSONRecovery: () => attemptJSONRecovery,
|
|
33
36
|
default: () => index_default,
|
|
34
37
|
log: () => logger_default
|
|
35
38
|
});
|
|
@@ -79,6 +82,7 @@ var DEFAULT_THINKING_CONFIG = {
|
|
|
79
82
|
thinkingBudget: 0,
|
|
80
83
|
thinkingLevel: import_genai.ThinkingLevel.MINIMAL
|
|
81
84
|
};
|
|
85
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 1e5;
|
|
82
86
|
var THINKING_SUPPORTED_MODELS = [
|
|
83
87
|
/^gemini-3-flash(-preview)?$/,
|
|
84
88
|
/^gemini-3-pro(-preview|-image-preview)?$/,
|
|
@@ -166,21 +170,43 @@ function AITransformFactory(options = {}) {
|
|
|
166
170
|
...options.chatConfig,
|
|
167
171
|
systemInstruction: this.systemInstructions
|
|
168
172
|
};
|
|
173
|
+
if (options.maxOutputTokens !== void 0) {
|
|
174
|
+
if (options.maxOutputTokens === null) {
|
|
175
|
+
delete this.chatConfig.maxOutputTokens;
|
|
176
|
+
} else {
|
|
177
|
+
this.chatConfig.maxOutputTokens = options.maxOutputTokens;
|
|
178
|
+
}
|
|
179
|
+
} else if (options.chatConfig?.maxOutputTokens !== void 0) {
|
|
180
|
+
if (options.chatConfig.maxOutputTokens === null) {
|
|
181
|
+
delete this.chatConfig.maxOutputTokens;
|
|
182
|
+
} else {
|
|
183
|
+
this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
187
|
+
}
|
|
169
188
|
const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(
|
|
170
189
|
(pattern) => pattern.test(this.modelName)
|
|
171
190
|
);
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
if (options.thinkingConfig !== void 0) {
|
|
192
|
+
if (options.thinkingConfig === null) {
|
|
193
|
+
delete this.chatConfig.thinkingConfig;
|
|
194
|
+
if (logger_default.level !== "silent") {
|
|
195
|
+
logger_default.debug(`thinkingConfig set to null - removed from configuration`);
|
|
196
|
+
}
|
|
197
|
+
} else if (modelSupportsThinking) {
|
|
198
|
+
const thinkingConfig = {
|
|
199
|
+
...DEFAULT_THINKING_CONFIG,
|
|
200
|
+
...options.thinkingConfig
|
|
201
|
+
};
|
|
202
|
+
this.chatConfig.thinkingConfig = thinkingConfig;
|
|
203
|
+
if (logger_default.level !== "silent") {
|
|
204
|
+
logger_default.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
if (logger_default.level !== "silent") {
|
|
208
|
+
logger_default.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
|
|
209
|
+
}
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
212
|
if (options.responseSchema) {
|
|
@@ -197,12 +223,17 @@ function AITransformFactory(options = {}) {
|
|
|
197
223
|
this.retryDelay = options.retryDelay || 1e3;
|
|
198
224
|
this.asyncValidator = options.asyncValidator || null;
|
|
199
225
|
this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
|
|
226
|
+
this.enableGrounding = options.enableGrounding || false;
|
|
227
|
+
this.groundingConfig = options.groundingConfig || {};
|
|
200
228
|
if (this.promptKey === this.answerKey) {
|
|
201
229
|
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
202
230
|
}
|
|
203
231
|
if (logger_default.level !== "silent") {
|
|
204
232
|
logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
|
|
205
233
|
logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
234
|
+
logger_default.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
|
|
235
|
+
logger_default.debug(`Using API key: ${this.apiKey.substring(0, 10)}...`);
|
|
236
|
+
logger_default.debug(`Grounding ${this.enableGrounding ? "ENABLED" : "DISABLED"} (costs $35/1k queries)`);
|
|
206
237
|
}
|
|
207
238
|
const ai = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
|
|
208
239
|
this.genAIClient = ai;
|
|
@@ -211,12 +242,19 @@ function AITransformFactory(options = {}) {
|
|
|
211
242
|
async function initChat(force = false) {
|
|
212
243
|
if (this.chat && !force) return;
|
|
213
244
|
logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
214
|
-
|
|
245
|
+
const chatOptions = {
|
|
215
246
|
model: this.modelName,
|
|
216
247
|
// @ts-ignore
|
|
217
248
|
config: this.chatConfig,
|
|
218
249
|
history: []
|
|
219
|
-
}
|
|
250
|
+
};
|
|
251
|
+
if (this.enableGrounding) {
|
|
252
|
+
chatOptions.config.tools = [{
|
|
253
|
+
googleSearch: this.groundingConfig
|
|
254
|
+
}];
|
|
255
|
+
logger_default.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
|
|
256
|
+
}
|
|
257
|
+
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
220
258
|
try {
|
|
221
259
|
await this.genAIClient.models.list();
|
|
222
260
|
logger_default.debug("Gemini API connection successful.");
|
|
@@ -320,6 +358,32 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
|
|
|
320
358
|
}
|
|
321
359
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
322
360
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
361
|
+
const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
|
|
362
|
+
const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
|
|
363
|
+
if (enableGroundingForMessage !== this.enableGrounding) {
|
|
364
|
+
const originalGrounding = this.enableGrounding;
|
|
365
|
+
const originalConfig = this.groundingConfig;
|
|
366
|
+
try {
|
|
367
|
+
this.enableGrounding = enableGroundingForMessage;
|
|
368
|
+
this.groundingConfig = groundingConfigForMessage;
|
|
369
|
+
await this.init(true);
|
|
370
|
+
if (enableGroundingForMessage) {
|
|
371
|
+
logger_default.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
|
|
372
|
+
} else {
|
|
373
|
+
logger_default.debug(`Search grounding DISABLED for this message`);
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
this.enableGrounding = originalGrounding;
|
|
377
|
+
this.groundingConfig = originalConfig;
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
const restoreGrounding = async () => {
|
|
381
|
+
this.enableGrounding = originalGrounding;
|
|
382
|
+
this.groundingConfig = originalConfig;
|
|
383
|
+
await this.init(true);
|
|
384
|
+
};
|
|
385
|
+
options._restoreGrounding = restoreGrounding;
|
|
386
|
+
}
|
|
323
387
|
let lastError = null;
|
|
324
388
|
let lastPayload = null;
|
|
325
389
|
if (sourcePayload && isJSON(sourcePayload)) {
|
|
@@ -341,12 +405,18 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
|
|
|
341
405
|
await validatorFn(transformedPayload);
|
|
342
406
|
}
|
|
343
407
|
logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
408
|
+
if (options._restoreGrounding) {
|
|
409
|
+
await options._restoreGrounding();
|
|
410
|
+
}
|
|
344
411
|
return transformedPayload;
|
|
345
412
|
} catch (error) {
|
|
346
413
|
lastError = error;
|
|
347
414
|
logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
348
415
|
if (attempt >= maxRetries) {
|
|
349
416
|
logger_default.error(`All ${maxRetries + 1} attempts failed.`);
|
|
417
|
+
if (options._restoreGrounding) {
|
|
418
|
+
await options._restoreGrounding();
|
|
419
|
+
}
|
|
350
420
|
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
351
421
|
}
|
|
352
422
|
const delay = retryDelay * Math.pow(2, attempt);
|
|
@@ -405,12 +475,19 @@ async function estimateTokenUsage(nextPayload) {
|
|
|
405
475
|
async function resetChat() {
|
|
406
476
|
if (this.chat) {
|
|
407
477
|
logger_default.debug("Resetting Gemini chat session...");
|
|
408
|
-
|
|
478
|
+
const chatOptions = {
|
|
409
479
|
model: this.modelName,
|
|
410
480
|
// @ts-ignore
|
|
411
481
|
config: this.chatConfig,
|
|
412
482
|
history: []
|
|
413
|
-
}
|
|
483
|
+
};
|
|
484
|
+
if (this.enableGrounding) {
|
|
485
|
+
chatOptions.config.tools = [{
|
|
486
|
+
googleSearch: this.groundingConfig
|
|
487
|
+
}];
|
|
488
|
+
logger_default.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
|
|
489
|
+
}
|
|
490
|
+
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
414
491
|
logger_default.debug("Chat session reset.");
|
|
415
492
|
} else {
|
|
416
493
|
logger_default.warn("Cannot reset chat session: chat not yet initialized.");
|
|
@@ -423,6 +500,122 @@ function getChatHistory() {
|
|
|
423
500
|
}
|
|
424
501
|
return this.chat.getHistory();
|
|
425
502
|
}
|
|
503
|
+
function attemptJSONRecovery(text, maxAttempts = 100) {
|
|
504
|
+
if (!text || typeof text !== "string") return null;
|
|
505
|
+
try {
|
|
506
|
+
return JSON.parse(text);
|
|
507
|
+
} catch (e) {
|
|
508
|
+
}
|
|
509
|
+
let workingText = text.trim();
|
|
510
|
+
let braces = 0;
|
|
511
|
+
let brackets = 0;
|
|
512
|
+
let inString = false;
|
|
513
|
+
let escapeNext = false;
|
|
514
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
515
|
+
const char = workingText[j];
|
|
516
|
+
if (escapeNext) {
|
|
517
|
+
escapeNext = false;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (char === "\\") {
|
|
521
|
+
escapeNext = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (char === '"') {
|
|
525
|
+
inString = !inString;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!inString) {
|
|
529
|
+
if (char === "{") braces++;
|
|
530
|
+
else if (char === "}") braces--;
|
|
531
|
+
else if (char === "[") brackets++;
|
|
532
|
+
else if (char === "]") brackets--;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
|
|
536
|
+
let fixedText = workingText;
|
|
537
|
+
if (inString) {
|
|
538
|
+
fixedText += '"';
|
|
539
|
+
}
|
|
540
|
+
while (braces > 0) {
|
|
541
|
+
fixedText += "}";
|
|
542
|
+
braces--;
|
|
543
|
+
}
|
|
544
|
+
while (brackets > 0) {
|
|
545
|
+
fixedText += "]";
|
|
546
|
+
brackets--;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const result = JSON.parse(fixedText);
|
|
550
|
+
if (logger_default.level !== "silent") {
|
|
551
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
} catch (e) {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
|
|
558
|
+
workingText = workingText.slice(0, -1);
|
|
559
|
+
let braces2 = 0;
|
|
560
|
+
let brackets2 = 0;
|
|
561
|
+
let inString2 = false;
|
|
562
|
+
let escapeNext2 = false;
|
|
563
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
564
|
+
const char = workingText[j];
|
|
565
|
+
if (escapeNext2) {
|
|
566
|
+
escapeNext2 = false;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (char === "\\") {
|
|
570
|
+
escapeNext2 = true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (char === '"') {
|
|
574
|
+
inString2 = !inString2;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (!inString2) {
|
|
578
|
+
if (char === "{") braces2++;
|
|
579
|
+
else if (char === "}") braces2--;
|
|
580
|
+
else if (char === "[") brackets2++;
|
|
581
|
+
else if (char === "]") brackets2--;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (braces2 === 0 && brackets2 === 0 && !inString2) {
|
|
585
|
+
try {
|
|
586
|
+
const result = JSON.parse(workingText);
|
|
587
|
+
if (logger_default.level !== "silent") {
|
|
588
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
} catch (e) {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (i > 5) {
|
|
595
|
+
let fixedText = workingText;
|
|
596
|
+
if (inString2) {
|
|
597
|
+
fixedText += '"';
|
|
598
|
+
}
|
|
599
|
+
while (braces2 > 0) {
|
|
600
|
+
fixedText += "}";
|
|
601
|
+
braces2--;
|
|
602
|
+
}
|
|
603
|
+
while (brackets2 > 0) {
|
|
604
|
+
fixedText += "]";
|
|
605
|
+
brackets2--;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const result = JSON.parse(fixedText);
|
|
609
|
+
if (logger_default.level !== "silent") {
|
|
610
|
+
logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
611
|
+
}
|
|
612
|
+
return result;
|
|
613
|
+
} catch (e) {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
426
619
|
function isJSON(data) {
|
|
427
620
|
try {
|
|
428
621
|
const attempt = JSON.stringify(data);
|
|
@@ -497,6 +690,10 @@ function extractJSON(text) {
|
|
|
497
690
|
if (isJSONStr(cleanedText)) {
|
|
498
691
|
return JSON.parse(cleanedText);
|
|
499
692
|
}
|
|
693
|
+
const recoveredJSON = attemptJSONRecovery(text);
|
|
694
|
+
if (recoveredJSON !== null) {
|
|
695
|
+
return recoveredJSON;
|
|
696
|
+
}
|
|
500
697
|
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
501
698
|
}
|
|
502
699
|
function findCompleteJSONStructures(text) {
|
|
@@ -603,6 +800,9 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
603
800
|
}
|
|
604
801
|
// Annotate the CommonJS export names for ESM import in node:
|
|
605
802
|
0 && (module.exports = {
|
|
803
|
+
HarmBlockThreshold,
|
|
804
|
+
HarmCategory,
|
|
606
805
|
ThinkingLevel,
|
|
806
|
+
attemptJSONRecovery,
|
|
607
807
|
log
|
|
608
808
|
});
|
package/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import u from 'ak-tools';
|
|
|
28
28
|
import path from 'path';
|
|
29
29
|
import log from './logger.js';
|
|
30
30
|
export { log };
|
|
31
|
-
export { ThinkingLevel };
|
|
31
|
+
export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
|
|
@@ -57,6 +57,8 @@ const DEFAULT_THINKING_CONFIG = {
|
|
|
57
57
|
thinkingLevel: ThinkingLevel.MINIMAL
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 100000; // Default ceiling for output tokens
|
|
61
|
+
|
|
60
62
|
// Models that support thinking features (as of Dec 2024)
|
|
61
63
|
// Using regex patterns for more precise matching
|
|
62
64
|
const THINKING_SUPPORTED_MODELS = [
|
|
@@ -136,6 +138,7 @@ class AITransformer {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
export default AITransformer;
|
|
141
|
+
export { attemptJSONRecovery }; // Export for testing
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
144
|
* factory function to create an AI Transformer instance
|
|
@@ -186,25 +189,53 @@ function AITransformFactory(options = {}) {
|
|
|
186
189
|
systemInstruction: this.systemInstructions
|
|
187
190
|
};
|
|
188
191
|
|
|
192
|
+
// Handle maxOutputTokens with explicit null check
|
|
193
|
+
// Priority: options.maxOutputTokens > options.chatConfig.maxOutputTokens > DEFAULT
|
|
194
|
+
// Setting to null explicitly removes the limit
|
|
195
|
+
if (options.maxOutputTokens !== undefined) {
|
|
196
|
+
if (options.maxOutputTokens === null) {
|
|
197
|
+
delete this.chatConfig.maxOutputTokens;
|
|
198
|
+
} else {
|
|
199
|
+
this.chatConfig.maxOutputTokens = options.maxOutputTokens;
|
|
200
|
+
}
|
|
201
|
+
} else if (options.chatConfig?.maxOutputTokens !== undefined) {
|
|
202
|
+
if (options.chatConfig.maxOutputTokens === null) {
|
|
203
|
+
delete this.chatConfig.maxOutputTokens;
|
|
204
|
+
} else {
|
|
205
|
+
this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
209
|
+
}
|
|
210
|
+
|
|
189
211
|
// Only add thinkingConfig if the model supports it
|
|
190
212
|
const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(pattern =>
|
|
191
213
|
pattern.test(this.modelName)
|
|
192
214
|
);
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
// Handle thinkingConfig - null explicitly removes it, undefined means not specified
|
|
217
|
+
if (options.thinkingConfig !== undefined) {
|
|
218
|
+
if (options.thinkingConfig === null) {
|
|
219
|
+
// Explicitly remove thinkingConfig if set to null
|
|
220
|
+
delete this.chatConfig.thinkingConfig;
|
|
221
|
+
if (log.level !== 'silent') {
|
|
222
|
+
log.debug(`thinkingConfig set to null - removed from configuration`);
|
|
223
|
+
}
|
|
224
|
+
} else if (modelSupportsThinking) {
|
|
225
|
+
// Handle thinkingConfig - merge with defaults
|
|
226
|
+
const thinkingConfig = {
|
|
227
|
+
...DEFAULT_THINKING_CONFIG,
|
|
228
|
+
...options.thinkingConfig
|
|
229
|
+
};
|
|
230
|
+
this.chatConfig.thinkingConfig = thinkingConfig;
|
|
231
|
+
|
|
232
|
+
if (log.level !== 'silent') {
|
|
233
|
+
log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
if (log.level !== 'silent') {
|
|
237
|
+
log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
|
|
238
|
+
}
|
|
208
239
|
}
|
|
209
240
|
}
|
|
210
241
|
|
|
@@ -234,6 +265,10 @@ function AITransformFactory(options = {}) {
|
|
|
234
265
|
//are we forcing json responses only?
|
|
235
266
|
this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
|
|
236
267
|
|
|
268
|
+
// Grounding configuration (disabled by default to avoid costs)
|
|
269
|
+
this.enableGrounding = options.enableGrounding || false;
|
|
270
|
+
this.groundingConfig = options.groundingConfig || {};
|
|
271
|
+
|
|
237
272
|
if (this.promptKey === this.answerKey) {
|
|
238
273
|
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
239
274
|
}
|
|
@@ -241,6 +276,10 @@ function AITransformFactory(options = {}) {
|
|
|
241
276
|
if (log.level !== 'silent') {
|
|
242
277
|
log.debug(`Creating AI Transformer with model: ${this.modelName}`);
|
|
243
278
|
log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
279
|
+
log.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
|
|
280
|
+
// Log API key prefix for tracking (first 10 chars only for security)
|
|
281
|
+
log.debug(`Using API key: ${this.apiKey.substring(0, 10)}...`);
|
|
282
|
+
log.debug(`Grounding ${this.enableGrounding ? 'ENABLED' : 'DISABLED'} (costs $35/1k queries)`);
|
|
244
283
|
}
|
|
245
284
|
|
|
246
285
|
const ai = new GoogleGenAI({ apiKey: this.apiKey });
|
|
@@ -259,12 +298,23 @@ async function initChat(force = false) {
|
|
|
259
298
|
|
|
260
299
|
log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
261
300
|
|
|
262
|
-
|
|
301
|
+
// Add grounding tools if enabled
|
|
302
|
+
const chatOptions = {
|
|
263
303
|
model: this.modelName,
|
|
264
304
|
// @ts-ignore
|
|
265
305
|
config: this.chatConfig,
|
|
266
306
|
history: [],
|
|
267
|
-
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Only add tools if grounding is explicitly enabled
|
|
310
|
+
if (this.enableGrounding) {
|
|
311
|
+
chatOptions.config.tools = [{
|
|
312
|
+
googleSearch: this.groundingConfig
|
|
313
|
+
}];
|
|
314
|
+
log.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
268
318
|
|
|
269
319
|
try {
|
|
270
320
|
await this.genAIClient.models.list();
|
|
@@ -431,6 +481,47 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
|
|
|
431
481
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
432
482
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
433
483
|
|
|
484
|
+
// Check if grounding should be enabled for this specific message
|
|
485
|
+
const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
|
|
486
|
+
const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
|
|
487
|
+
|
|
488
|
+
// Reinitialize chat if grounding settings changed for this message
|
|
489
|
+
if (enableGroundingForMessage !== this.enableGrounding) {
|
|
490
|
+
const originalGrounding = this.enableGrounding;
|
|
491
|
+
const originalConfig = this.groundingConfig;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
// Temporarily change grounding settings
|
|
495
|
+
this.enableGrounding = enableGroundingForMessage;
|
|
496
|
+
this.groundingConfig = groundingConfigForMessage;
|
|
497
|
+
|
|
498
|
+
// Force reinit with new settings
|
|
499
|
+
await this.init(true);
|
|
500
|
+
|
|
501
|
+
// Log the change
|
|
502
|
+
if (enableGroundingForMessage) {
|
|
503
|
+
log.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
|
|
504
|
+
} else {
|
|
505
|
+
log.debug(`Search grounding DISABLED for this message`);
|
|
506
|
+
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
// Restore original settings on error
|
|
509
|
+
this.enableGrounding = originalGrounding;
|
|
510
|
+
this.groundingConfig = originalConfig;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Schedule restoration after message completes
|
|
515
|
+
const restoreGrounding = async () => {
|
|
516
|
+
this.enableGrounding = originalGrounding;
|
|
517
|
+
this.groundingConfig = originalConfig;
|
|
518
|
+
await this.init(true);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Store restoration function to call after message completes
|
|
522
|
+
options._restoreGrounding = restoreGrounding;
|
|
523
|
+
}
|
|
524
|
+
|
|
434
525
|
let lastError = null;
|
|
435
526
|
let lastPayload = null; // Store the payload that caused the validation error
|
|
436
527
|
|
|
@@ -466,6 +557,12 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
|
|
|
466
557
|
|
|
467
558
|
// Step 3: Success!
|
|
468
559
|
log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
560
|
+
|
|
561
|
+
// Restore original grounding settings if they were changed
|
|
562
|
+
if (options._restoreGrounding) {
|
|
563
|
+
await options._restoreGrounding();
|
|
564
|
+
}
|
|
565
|
+
|
|
469
566
|
return transformedPayload;
|
|
470
567
|
|
|
471
568
|
} catch (error) {
|
|
@@ -474,6 +571,12 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
|
|
|
474
571
|
|
|
475
572
|
if (attempt >= maxRetries) {
|
|
476
573
|
log.error(`All ${maxRetries + 1} attempts failed.`);
|
|
574
|
+
|
|
575
|
+
// Restore original grounding settings even on failure
|
|
576
|
+
if (options._restoreGrounding) {
|
|
577
|
+
await options._restoreGrounding();
|
|
578
|
+
}
|
|
579
|
+
|
|
477
580
|
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
478
581
|
}
|
|
479
582
|
|
|
@@ -575,12 +678,24 @@ async function estimateTokenUsage(nextPayload) {
|
|
|
575
678
|
async function resetChat() {
|
|
576
679
|
if (this.chat) {
|
|
577
680
|
log.debug("Resetting Gemini chat session...");
|
|
578
|
-
|
|
681
|
+
|
|
682
|
+
// Prepare chat options with grounding if enabled
|
|
683
|
+
const chatOptions = {
|
|
579
684
|
model: this.modelName,
|
|
580
685
|
// @ts-ignore
|
|
581
686
|
config: this.chatConfig,
|
|
582
687
|
history: [],
|
|
583
|
-
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// Only add tools if grounding is explicitly enabled
|
|
691
|
+
if (this.enableGrounding) {
|
|
692
|
+
chatOptions.config.tools = [{
|
|
693
|
+
googleSearch: this.groundingConfig
|
|
694
|
+
}];
|
|
695
|
+
log.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
584
699
|
log.debug("Chat session reset.");
|
|
585
700
|
} else {
|
|
586
701
|
log.warn("Cannot reset chat session: chat not yet initialized.");
|
|
@@ -606,6 +721,173 @@ HELPERS
|
|
|
606
721
|
----
|
|
607
722
|
*/
|
|
608
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Attempts to recover truncated JSON by progressively removing characters from the end
|
|
726
|
+
* until valid JSON is found or recovery fails
|
|
727
|
+
* @param {string} text - The potentially truncated JSON string
|
|
728
|
+
* @param {number} maxAttempts - Maximum number of characters to remove
|
|
729
|
+
* @returns {Object|null} - Parsed JSON object or null if recovery fails
|
|
730
|
+
*/
|
|
731
|
+
function attemptJSONRecovery(text, maxAttempts = 100) {
|
|
732
|
+
if (!text || typeof text !== 'string') return null;
|
|
733
|
+
|
|
734
|
+
// First, try parsing as-is
|
|
735
|
+
try {
|
|
736
|
+
return JSON.parse(text);
|
|
737
|
+
} catch (e) {
|
|
738
|
+
// Continue with recovery
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let workingText = text.trim();
|
|
742
|
+
|
|
743
|
+
// First attempt: try to close unclosed structures without removing characters
|
|
744
|
+
// Count open/close braces and brackets in the original text
|
|
745
|
+
let braces = 0;
|
|
746
|
+
let brackets = 0;
|
|
747
|
+
let inString = false;
|
|
748
|
+
let escapeNext = false;
|
|
749
|
+
|
|
750
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
751
|
+
const char = workingText[j];
|
|
752
|
+
|
|
753
|
+
if (escapeNext) {
|
|
754
|
+
escapeNext = false;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (char === '\\') {
|
|
759
|
+
escapeNext = true;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (char === '"') {
|
|
764
|
+
inString = !inString;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!inString) {
|
|
769
|
+
if (char === '{') braces++;
|
|
770
|
+
else if (char === '}') braces--;
|
|
771
|
+
else if (char === '[') brackets++;
|
|
772
|
+
else if (char === ']') brackets--;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Try to fix by just adding closing characters
|
|
777
|
+
if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
|
|
778
|
+
let fixedText = workingText;
|
|
779
|
+
|
|
780
|
+
// Close any open strings first
|
|
781
|
+
if (inString) {
|
|
782
|
+
fixedText += '"';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Add missing closing characters
|
|
786
|
+
while (braces > 0) {
|
|
787
|
+
fixedText += '}';
|
|
788
|
+
braces--;
|
|
789
|
+
}
|
|
790
|
+
while (brackets > 0) {
|
|
791
|
+
fixedText += ']';
|
|
792
|
+
brackets--;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
const result = JSON.parse(fixedText);
|
|
797
|
+
if (log.level !== 'silent') {
|
|
798
|
+
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
799
|
+
}
|
|
800
|
+
return result;
|
|
801
|
+
} catch (e) {
|
|
802
|
+
// Simple fix didn't work, continue with more aggressive recovery
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Second attempt: progressively remove characters from the end
|
|
807
|
+
|
|
808
|
+
for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
|
|
809
|
+
// Remove one character from the end
|
|
810
|
+
workingText = workingText.slice(0, -1);
|
|
811
|
+
|
|
812
|
+
// Count open/close braces and brackets
|
|
813
|
+
let braces = 0;
|
|
814
|
+
let brackets = 0;
|
|
815
|
+
let inString = false;
|
|
816
|
+
let escapeNext = false;
|
|
817
|
+
|
|
818
|
+
for (let j = 0; j < workingText.length; j++) {
|
|
819
|
+
const char = workingText[j];
|
|
820
|
+
|
|
821
|
+
if (escapeNext) {
|
|
822
|
+
escapeNext = false;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (char === '\\') {
|
|
827
|
+
escapeNext = true;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (char === '"') {
|
|
832
|
+
inString = !inString;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!inString) {
|
|
837
|
+
if (char === '{') braces++;
|
|
838
|
+
else if (char === '}') braces--;
|
|
839
|
+
else if (char === '[') brackets++;
|
|
840
|
+
else if (char === ']') brackets--;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// If we have balanced braces/brackets, try parsing
|
|
845
|
+
if (braces === 0 && brackets === 0 && !inString) {
|
|
846
|
+
try {
|
|
847
|
+
const result = JSON.parse(workingText);
|
|
848
|
+
if (log.level !== 'silent') {
|
|
849
|
+
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
|
|
850
|
+
}
|
|
851
|
+
return result;
|
|
852
|
+
} catch (e) {
|
|
853
|
+
// Continue trying
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// After a few attempts, try adding closing characters
|
|
858
|
+
if (i > 5) {
|
|
859
|
+
let fixedText = workingText;
|
|
860
|
+
|
|
861
|
+
// Close any open strings first
|
|
862
|
+
if (inString) {
|
|
863
|
+
fixedText += '"';
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Add missing closing characters
|
|
867
|
+
while (braces > 0) {
|
|
868
|
+
fixedText += '}';
|
|
869
|
+
braces--;
|
|
870
|
+
}
|
|
871
|
+
while (brackets > 0) {
|
|
872
|
+
fixedText += ']';
|
|
873
|
+
brackets--;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
const result = JSON.parse(fixedText);
|
|
878
|
+
if (log.level !== 'silent') {
|
|
879
|
+
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
880
|
+
}
|
|
881
|
+
return result;
|
|
882
|
+
} catch (e) {
|
|
883
|
+
// Recovery failed, continue trying
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
|
|
609
891
|
function isJSON(data) {
|
|
610
892
|
try {
|
|
611
893
|
const attempt = JSON.stringify(data);
|
|
@@ -703,6 +985,13 @@ function extractJSON(text) {
|
|
|
703
985
|
return JSON.parse(cleanedText);
|
|
704
986
|
}
|
|
705
987
|
|
|
988
|
+
// Strategy 6: Last resort - attempt recovery for potentially truncated JSON
|
|
989
|
+
// This is especially useful when maxOutputTokens might have cut off the response
|
|
990
|
+
const recoveredJSON = attemptJSONRecovery(text);
|
|
991
|
+
if (recoveredJSON !== null) {
|
|
992
|
+
return recoveredJSON;
|
|
993
|
+
}
|
|
994
|
+
|
|
706
995
|
// If all else fails, throw an error with helpful information
|
|
707
996
|
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
708
997
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ak-gemini",
|
|
3
3
|
"author": "ak@mixpanel.com",
|
|
4
4
|
"description": "AK's Generative AI Helper for doing... transforms",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.9",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"prune": "rm -rf tmp/*",
|
|
40
40
|
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
41
41
|
"test:unit": "npm test -- tests/module.test.js",
|
|
42
|
+
"test:fixed": "npm test -- --testNamePattern=\"should use context in the prompt and transform accordingly|should augment the payload as instructed by system instructions|should succeed on the first try if validation passes|should handle invalid model names|should handle multiple concurrent messages|should use the constructor-provided asyncValidator|should override system instructions from the file\"",
|
|
42
43
|
"build:cjs": "esbuild index.js --bundle --platform=node --format=cjs --outfile=index.cjs --external:@google/genai --external:ak-tools --external:dotenv --external:pino-pretty --external:pino",
|
|
43
44
|
"coverage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
44
45
|
"typecheck": "tsc --noEmit",
|
package/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { GoogleGenAI, ThinkingLevel } from '@google/genai';
|
|
1
|
+
import type { GoogleGenAI, ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
2
2
|
|
|
3
|
-
export { ThinkingLevel };
|
|
3
|
+
export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
|
|
4
4
|
|
|
5
5
|
export interface ThinkingConfig {
|
|
6
6
|
/** Indicates whether to include thoughts in the response. If true, thoughts are returned only if the model supports thought and thoughts are available. */
|
|
@@ -12,8 +12,8 @@ export interface ThinkingConfig {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface SafetySetting {
|
|
15
|
-
category:
|
|
16
|
-
threshold:
|
|
15
|
+
category: HarmCategory; // The harm category
|
|
16
|
+
threshold: HarmBlockThreshold; // The blocking threshold
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface ChatConfig {
|
|
@@ -21,6 +21,7 @@ export interface ChatConfig {
|
|
|
21
21
|
temperature?: number; // Controls randomness (0.0 to 1.0)
|
|
22
22
|
topP?: number; // Controls diversity via nucleus sampling
|
|
23
23
|
topK?: number; // Controls diversity by limiting top-k tokens
|
|
24
|
+
maxOutputTokens?: number; // Maximum number of tokens that can be generated in the response
|
|
24
25
|
systemInstruction?: string; // System instruction for the model
|
|
25
26
|
safetySettings?: SafetySetting[]; // Safety settings array
|
|
26
27
|
responseSchema?: Object; // Schema for validating model responses
|
|
@@ -50,7 +51,9 @@ export interface AITransformerContext {
|
|
|
50
51
|
rawMessage?: (payload: Record<string, unknown> | string) => Promise<Record<string, unknown>>; // Function to send raw messages to the model
|
|
51
52
|
genAIClient?: GoogleGenAI; // Google GenAI client instance
|
|
52
53
|
onlyJSON?: boolean; // If true, only JSON responses are allowed
|
|
53
|
-
|
|
54
|
+
enableGrounding?: boolean; // Enable Google Search grounding (default: false, WARNING: costs $35/1k queries)
|
|
55
|
+
groundingConfig?: Record<string, any>; // Additional grounding configuration options
|
|
56
|
+
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export interface TransformationExample {
|
|
@@ -74,6 +77,7 @@ export interface AITransformerOptions {
|
|
|
74
77
|
systemInstructions?: string; // Custom system instructions for the model
|
|
75
78
|
chatConfig?: ChatConfig; // Configuration object for the chat session
|
|
76
79
|
thinkingConfig?: ThinkingConfig; // Thinking features configuration (defaults to thinkingBudget: 0, thinkingLevel: "MINIMAL")
|
|
80
|
+
maxOutputTokens?: number; // Maximum number of tokens that can be generated in the response (defaults to 100000)
|
|
77
81
|
examplesFile?: string; // Path to JSON file containing transformation examples
|
|
78
82
|
exampleData?: TransformationExample[]; // Inline examples to seed the transformer
|
|
79
83
|
sourceKey?: string; // Key name for source data in examples (alias for promptKey)
|
|
@@ -91,6 +95,8 @@ export interface AITransformerOptions {
|
|
|
91
95
|
onlyJSON?: boolean; // If true, only JSON responses are allowed
|
|
92
96
|
asyncValidator?: AsyncValidatorFunction; // Optional async validator function for response validation
|
|
93
97
|
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none'; // Log level for the logger (defaults to 'info', 'none' disables logging)
|
|
98
|
+
enableGrounding?: boolean; // Enable Google Search grounding (default: false, WARNING: costs $35/1k queries)
|
|
99
|
+
groundingConfig?: Record<string, any>; // Additional grounding configuration options
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
// Async validator function type
|
|
@@ -118,6 +124,8 @@ export declare class AITransformer {
|
|
|
118
124
|
genAIClient: any;
|
|
119
125
|
chat: any;
|
|
120
126
|
logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none';
|
|
127
|
+
enableGrounding: boolean;
|
|
128
|
+
groundingConfig: Record<string, any>;
|
|
121
129
|
|
|
122
130
|
// Methods
|
|
123
131
|
init(force?: boolean): Promise<void>;
|