ak-gemini 1.1.13 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,1503 +1,41 @@
1
1
  /**
2
- * @fileoverview
3
- * Generic AI transformation module that can be configured for different use cases.
4
- * Supports various models, system instructions, chat configurations, and example datasets.
5
- */
6
-
7
- /**
8
- * @typedef {import('./types').SafetySetting} SafetySetting
9
- * @typedef {import('./types').ChatConfig} ChatConfig
10
- * @typedef {import('./types').TransformationExample} TransformationExample
11
- * @typedef {import('./types').ExampleFileContent} ExampleFileContent
12
- * @typedef {import('./types').AITransformerOptions} AITransformerOptions
13
- * @typedef {import('./types').AsyncValidatorFunction} AsyncValidatorFunction
14
- * @typedef {import('./types').AITransformerContext} ExportedAPI
15
- *
16
- */
17
-
18
- //env
19
- import dotenv from 'dotenv';
20
- dotenv.config();
21
- const { NODE_ENV = "unknown", GEMINI_API_KEY, LOG_LEVEL = "" } = process.env;
22
-
23
-
24
-
25
- //deps
26
- import { GoogleGenAI, HarmCategory, HarmBlockThreshold, ThinkingLevel } from '@google/genai';
27
- import u from 'ak-tools';
28
- import path from 'path';
29
- import log from './logger.js';
30
- export { log };
31
- export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
32
-
33
-
34
-
35
- // defaults
36
- const DEFAULT_SAFETY_SETTINGS = [
37
- { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
38
- { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
39
- ];
40
-
41
- const DEFAULT_SYSTEM_INSTRUCTIONS = `
42
- You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
43
-
44
- You will be provided with example transformations (Source JSON -> Target JSON).
45
-
46
- Learn the mapping rules from these examples.
47
-
48
- When presented with new Source JSON, apply the learned transformation rules to produce a new Target JSON payload.
49
-
50
- Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
51
-
52
- Do not include any additional text, explanations, or formatting before or after the JSON object.
53
- `;
54
-
55
- const DEFAULT_THINKING_CONFIG = {
56
- thinkingBudget: 0
57
- };
58
-
59
- const DEFAULT_MAX_OUTPUT_TOKENS = 50_000; // Default ceiling for output tokens
60
-
61
- // Models that support thinking features (as of Dec 2024)
62
- // Using regex patterns for more precise matching
63
- const THINKING_SUPPORTED_MODELS = [
64
- /^gemini-3-flash(-preview)?$/,
65
- /^gemini-3-pro(-preview|-image-preview)?$/,
66
- /^gemini-2\.5-pro/,
67
- /^gemini-2\.5-flash(-preview)?$/,
68
- /^gemini-2\.5-flash-lite(-preview)?$/,
69
- /^gemini-2\.0-flash$/ // Experimental support, exact match only
70
- ];
71
-
72
- const DEFAULT_CHAT_CONFIG = {
73
- responseMimeType: 'application/json',
74
- temperature: 0.2,
75
- topP: 0.95,
76
- topK: 64,
77
- systemInstruction: DEFAULT_SYSTEM_INSTRUCTIONS,
78
- safetySettings: DEFAULT_SAFETY_SETTINGS
79
- };
80
-
81
- /**
82
- * @typedef {import('./types').AITransformer} AITransformerUtility
83
- */
84
-
85
-
86
-
87
- /**
88
- * main export class for AI Transformer
89
- * @class AITransformer
90
- * @type {AITransformerUtility}
91
- * @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
92
- * @implements {ExportedAPI}
93
- */
94
- class AITransformer {
95
- /**
96
- * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
97
- *
98
- */
99
- constructor(options = {}) {
100
- this.modelName = "";
101
- this.promptKey = "";
102
- this.answerKey = "";
103
- this.contextKey = "";
104
- this.explanationKey = "";
105
- this.systemInstructionKey = "";
106
- this.maxRetries = 3;
107
- this.retryDelay = 1000;
108
- // this.systemInstructions = "";
109
- this.chatConfig = {};
110
- this.apiKey = GEMINI_API_KEY;
111
- this.onlyJSON = true; // always return JSON
112
- this.asyncValidator = null; // for transformWithValidation
113
- this.logLevel = 'info'; // default log level
114
- this.lastResponseMetadata = null; // stores metadata from last API response
115
- this.exampleCount = 0; // tracks number of example history items from seed()
116
- // Cumulative usage tracking across retry attempts
117
- this._cumulativeUsage = {
118
- promptTokens: 0,
119
- responseTokens: 0,
120
- totalTokens: 0,
121
- attempts: 0
122
- };
123
- AITransformFactory.call(this, options);
124
-
125
- //external API
126
- this.init = initChat.bind(this);
127
- this.seed = seedWithExamples.bind(this);
128
-
129
- // Internal "raw" message sender
130
- this.rawMessage = rawMessage.bind(this);
131
-
132
- // The public `.message()` method uses the GLOBAL validator
133
- this.message = (payload, opts = {}, validatorFn = null) => {
134
-
135
- return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
136
- };
137
-
138
- this.rebuild = rebuildPayload.bind(this);
139
- this.reset = resetChat.bind(this);
140
- this.getHistory = getChatHistory.bind(this);
141
- this.messageAndValidate = prepareAndValidateMessage.bind(this);
142
- this.transformWithValidation = prepareAndValidateMessage.bind(this);
143
- this.estimate = estimateInputTokens.bind(this);
144
- this.updateSystemInstructions = updateSystemInstructions.bind(this);
145
- this.estimateCost = estimateCost.bind(this);
146
- this.clearConversation = clearConversation.bind(this);
147
- this.getLastUsage = getLastUsage.bind(this);
148
- }
149
- }
150
-
151
- export default AITransformer;
152
- export { attemptJSONRecovery }; // Export for testing
153
-
154
- /**
155
- * factory function to create an AI Transformer instance
156
- * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
157
- * @returns {void} - An instance of AITransformer with initialized properties and methods
158
- */
159
- function AITransformFactory(options = {}) {
160
- // ? https://ai.google.dev/gemini-api/docs/models
161
- this.modelName = options.modelName || 'gemini-2.5-flash';
162
-
163
- // Only use default if systemInstructions was not provided at all
164
- if (options.systemInstructions === undefined) {
165
- this.systemInstructions = DEFAULT_SYSTEM_INSTRUCTIONS;
166
- } else {
167
- // Use the provided value (could be null, false, or a custom string)
168
- this.systemInstructions = options.systemInstructions;
169
- }
170
-
171
- // Configure log level - priority: options.logLevel > LOG_LEVEL env > NODE_ENV based defaults > 'info'
172
- if (options.logLevel) {
173
- this.logLevel = options.logLevel;
174
- if (this.logLevel === 'none') {
175
- // Set to silent to disable all logging
176
- log.level = 'silent';
177
- } else {
178
- // Set the log level as specified
179
- log.level = this.logLevel;
180
- }
181
- } else if (LOG_LEVEL) {
182
- // Use environment variable if no option specified
183
- this.logLevel = LOG_LEVEL;
184
- log.level = LOG_LEVEL;
185
- } else if (NODE_ENV === 'dev') {
186
- this.logLevel = 'debug';
187
- log.level = 'debug';
188
- } else if (NODE_ENV === 'test') {
189
- this.logLevel = 'warn';
190
- log.level = 'warn';
191
- } else if (NODE_ENV.startsWith('prod')) {
192
- this.logLevel = 'error';
193
- log.level = 'error';
194
- } else {
195
- // Default to info
196
- this.logLevel = 'info';
197
- log.level = 'info';
198
- }
199
-
200
- // Vertex AI configuration
201
- this.vertexai = options.vertexai || false;
202
- this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
203
- this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || undefined;
204
- this.googleAuthOptions = options.googleAuthOptions || null;
205
-
206
- // API Key (for Gemini API, not Vertex AI)
207
- this.apiKey = options.apiKey !== undefined && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
208
-
209
- // Validate authentication - need either API key (for Gemini API) or Vertex AI config
210
- if (!this.vertexai && !this.apiKey) {
211
- 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.");
212
- }
213
- if (this.vertexai && !this.project) {
214
- throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
215
- }
216
-
217
- // Build chat config, making sure systemInstruction uses the custom instructions
218
- this.chatConfig = {
219
- ...DEFAULT_CHAT_CONFIG,
220
- ...options.chatConfig
221
- };
222
-
223
- // Handle systemInstructions: use custom if provided, otherwise keep default from DEFAULT_CHAT_CONFIG
224
- // If explicitly set to null/false, remove it entirely
225
- if (this.systemInstructions) {
226
- this.chatConfig.systemInstruction = this.systemInstructions;
227
- } else if (options.systemInstructions !== undefined) {
228
- // Explicitly set to null/false/empty - remove system instruction
229
- delete this.chatConfig.systemInstruction;
230
- }
231
-
232
- // Handle maxOutputTokens with explicit null check
233
- // Priority: options.maxOutputTokens > options.chatConfig.maxOutputTokens > DEFAULT
234
- // Setting to null explicitly removes the limit
235
- if (options.maxOutputTokens !== undefined) {
236
- if (options.maxOutputTokens === null) {
237
- delete this.chatConfig.maxOutputTokens;
238
- } else {
239
- this.chatConfig.maxOutputTokens = options.maxOutputTokens;
240
- }
241
- } else if (options.chatConfig?.maxOutputTokens !== undefined) {
242
- if (options.chatConfig.maxOutputTokens === null) {
243
- delete this.chatConfig.maxOutputTokens;
244
- } else {
245
- this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
246
- }
247
- } else {
248
- this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
249
- }
250
-
251
- // Only add thinkingConfig if the model supports it
252
- const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(pattern =>
253
- pattern.test(this.modelName)
254
- );
255
-
256
- // Handle thinkingConfig - null explicitly removes it, undefined means not specified
257
- if (options.thinkingConfig !== undefined) {
258
- if (options.thinkingConfig === null) {
259
- // Explicitly remove thinkingConfig if set to null
260
- delete this.chatConfig.thinkingConfig;
261
- if (log.level !== 'silent') {
262
- log.debug(`thinkingConfig set to null - removed from configuration`);
263
- }
264
- } else if (modelSupportsThinking) {
265
- // Handle thinkingConfig - merge with defaults
266
- const thinkingConfig = {
267
- ...DEFAULT_THINKING_CONFIG,
268
- ...options.thinkingConfig
269
- };
270
-
271
- // Gemini API does not allow both thinkingBudget and thinkingLevel together.
272
- // If user specified thinkingLevel, remove thinkingBudget (user preference wins)
273
- if (options.thinkingConfig?.thinkingLevel !== undefined) {
274
- delete thinkingConfig.thinkingBudget;
275
- }
276
-
277
- this.chatConfig.thinkingConfig = thinkingConfig;
278
-
279
- if (log.level !== 'silent') {
280
- log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
281
- }
282
- } else {
283
- if (log.level !== 'silent') {
284
- log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
285
- }
286
- }
287
- }
288
-
289
- // response schema is optional, but if provided, it should be a valid JSON schema
290
- if (options.responseSchema) {
291
- this.chatConfig.responseSchema = options.responseSchema;
292
- }
293
-
294
- // examples file is optional, but if provided, it should contain valid PROMPT and ANSWER keys
295
- this.examplesFile = options.examplesFile || null;
296
- this.exampleData = options.exampleData || null; // can be used instead of examplesFile
297
-
298
- // Use configurable keys with fallbacks
299
- this.promptKey = options.promptKey || options.sourceKey || 'PROMPT';
300
- this.answerKey = options.answerKey || options.targetKey || 'ANSWER';
301
- this.contextKey = options.contextKey || 'CONTEXT'; // Optional key for context
302
- this.explanationKey = options.explanationKey || 'EXPLANATION'; // Optional key for explanations
303
- this.systemInstructionsKey = options.systemInstructionsKey || 'SYSTEM'; // Optional key for system instructions
304
-
305
- // Retry configuration
306
- this.maxRetries = options.maxRetries || 3;
307
- this.retryDelay = options.retryDelay || 1000;
308
-
309
- //allow async validation function
310
- this.asyncValidator = options.asyncValidator || null; // Function to validate transformed payloads
311
-
312
- //are we forcing json responses only?
313
- this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
314
-
315
- // Grounding configuration (disabled by default to avoid costs)
316
- this.enableGrounding = options.enableGrounding || false;
317
- this.groundingConfig = options.groundingConfig || {};
318
-
319
- // Billing labels for cost segmentation (Vertex AI only)
320
- this.labels = options.labels || {};
321
- if (Object.keys(this.labels).length > 0 && log.level !== 'silent') {
322
- if (!this.vertexai) {
323
- log.warn(`Billing labels are only supported with Vertex AI. Labels will be ignored.`);
324
- } else {
325
- log.debug(`Billing labels configured: ${JSON.stringify(this.labels)}`);
326
- }
327
- }
328
-
329
- if (this.promptKey === this.answerKey) {
330
- throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
331
- }
332
-
333
- if (log.level !== 'silent') {
334
- log.debug(`Creating AI Transformer with model: ${this.modelName}`);
335
- log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
336
- log.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
337
- // Log authentication method
338
- if (this.vertexai) {
339
- log.debug(`Using Vertex AI - Project: ${this.project}, Location: ${this.location || 'global (default)'}`);
340
- if (this.googleAuthOptions?.keyFilename) {
341
- log.debug(`Auth: Service account key file: ${this.googleAuthOptions.keyFilename}`);
342
- } else if (this.googleAuthOptions?.credentials) {
343
- log.debug(`Auth: Inline credentials provided`);
344
- } else {
345
- log.debug(`Auth: Application Default Credentials (ADC)`);
346
- }
347
- } else {
348
- log.debug(`Using Gemini API with key: ${this.apiKey.substring(0, 10)}...`);
349
- }
350
- log.debug(`Grounding ${this.enableGrounding ? 'ENABLED' : 'DISABLED'} (costs $35/1k queries)`);
351
- }
352
-
353
- // Initialize Google GenAI client with appropriate configuration
354
- const clientOptions = this.vertexai
355
- ? {
356
- vertexai: true,
357
- project: this.project,
358
- ...(this.location && { location: this.location }),
359
- ...(this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions })
360
- }
361
- : { apiKey: this.apiKey };
362
-
363
- const ai = new GoogleGenAI(clientOptions);
364
- this.genAIClient = ai;
365
- this.chat = null;
366
- }
367
-
368
- /**
369
- * Initializes the chat session with the specified model and configurations.
370
- * @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
371
- * @this {ExportedAPI}
372
- * @returns {Promise<void>}
373
- */
374
- async function initChat(force = false) {
375
- if (this.chat && !force) return;
376
-
377
- log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
378
-
379
- // Add grounding tools if enabled
380
- const chatOptions = {
381
- model: this.modelName,
382
- // @ts-ignore
383
- config: {
384
- ...this.chatConfig,
385
- ...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
386
- },
387
- history: [],
388
- };
389
-
390
- // Only add tools if grounding is explicitly enabled
391
- if (this.enableGrounding) {
392
- chatOptions.config.tools = [{
393
- googleSearch: this.groundingConfig
394
- }];
395
- log.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
396
- }
397
-
398
- this.chat = await this.genAIClient.chats.create(chatOptions);
399
-
400
- try {
401
- await this.genAIClient.models.list();
402
- log.debug("Gemini API connection successful.");
403
- } catch (e) {
404
- throw new Error(`Gemini chat initialization failed: ${e.message}`);
405
- }
406
-
407
-
408
-
409
- log.debug("Gemini chat session initialized.");
410
- }
411
-
412
- /**
413
- * Seeds the chat session with example transformations.
414
- * @this {ExportedAPI}
415
- * @param {TransformationExample[]} [examples] - An array of transformation examples.
416
- * @this {ExportedAPI}
417
- * @returns {Promise<void>}
418
- */
419
- async function seedWithExamples(examples) {
420
- await this.init();
421
-
422
- if (!examples || !Array.isArray(examples) || examples.length === 0) {
423
- if (this.examplesFile) {
424
- log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
425
- try {
426
- // @ts-ignore
427
- examples = await u.load(path.resolve(this.examplesFile), true);
428
- }
429
- catch (err) {
430
- throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
431
- }
432
- }
433
-
434
- else if (this.exampleData) {
435
- log.debug(`Using example data provided in options.`);
436
- if (Array.isArray(this.exampleData)) {
437
- examples = this.exampleData;
438
- } else {
439
- throw new Error(`Invalid example data provided. Expected an array of examples.`);
440
- }
441
- }
442
-
443
- else {
444
- log.debug("No examples provided and no examples file specified. Skipping seeding.");
445
- return;
446
- }
447
- }
448
-
449
- const instructionExample = examples.find(ex => ex[this.systemInstructionsKey]);
450
- if (instructionExample) {
451
- log.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
452
- this.systemInstructions = instructionExample[this.systemInstructionsKey];
453
- this.chatConfig.systemInstruction = this.systemInstructions;
454
- await this.init(true); // Reinitialize chat with new system instructions
455
- }
456
-
457
- log.debug(`Seeding chat with ${examples.length} transformation examples...`);
458
- const historyToAdd = [];
459
-
460
- for (const example of examples) {
461
- // Use the configurable keys from constructor
462
- const contextValue = example[this.contextKey] || "";
463
- const promptValue = example[this.promptKey] || "";
464
- const answerValue = example[this.answerKey] || "";
465
- const explanationValue = example[this.explanationKey] || "";
466
- let userText = "";
467
- let modelResponse = {};
468
-
469
- // Add context as user message with special formatting to make it part of the example flow
470
- if (contextValue) {
471
- let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
472
- // Prefix context to make it clear it's contextual information
473
- userText += `CONTEXT:\n${contextText}\n\n`;
474
- }
475
-
476
- if (promptValue) {
477
- let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
478
- userText += promptText;
479
- }
480
-
481
- if (answerValue) modelResponse.data = answerValue;
482
- if (explanationValue) modelResponse.explanation = explanationValue;
483
- const modelText = JSON.stringify(modelResponse, null, 2);
484
-
485
- if (userText.trim().length && modelText.trim().length > 0) {
486
- historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
487
- historyToAdd.push({ role: 'model', parts: [{ text: modelText.trim() }] });
488
- }
489
-
490
- }
491
-
492
-
493
- const currentHistory = this?.chat?.getHistory() || [];
494
- log.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
495
- this.chat = await this.genAIClient.chats.create({
496
- model: this.modelName,
497
- // @ts-ignore
498
- config: {
499
- ...this.chatConfig,
500
- ...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
501
- },
502
- history: [...currentHistory, ...historyToAdd],
503
- });
504
-
505
- // Track example count for clearConversation() and stateless messages
506
- this.exampleCount = currentHistory.length + historyToAdd.length;
507
-
508
- const newHistory = this.chat.getHistory();
509
- log.debug(`Created new chat session with ${newHistory.length} examples.`);
510
- return newHistory;
511
- }
512
-
513
- /**
514
- * Transforms a source JSON payload into a target JSON payload
515
- * @param {Object} sourcePayload - The source payload (as a JavaScript object).
516
- * @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
517
- * @throws {Error} If the transformation fails or returns invalid JSON.
518
- */
519
- /**
520
- * (Internal) Sends a single prompt to the model and parses the response.
521
- * No validation or retry logic.
522
- * @this {ExportedAPI}
523
- * @param {Object|string} sourcePayload - The source payload.
524
- * @param {Object} [messageOptions] - Optional per-message options (e.g., labels).
525
- * @returns {Promise<Object>} - The transformed payload.
526
- */
527
- async function rawMessage(sourcePayload, messageOptions = {}) {
528
- if (!this.chat) {
529
- throw new Error("Chat session not initialized.");
530
- }
531
-
532
- const actualPayload = typeof sourcePayload === 'string'
533
- ? sourcePayload
534
- : JSON.stringify(sourcePayload, null, 2);
535
-
536
- // Merge instance labels with per-message labels (per-message takes precedence)
537
- // Labels only supported with Vertex AI
538
- const mergedLabels = { ...this.labels, ...(messageOptions.labels || {}) };
539
- const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
540
-
541
- try {
542
- const sendParams = { message: actualPayload };
543
-
544
- // Add config with labels if we have any (Vertex AI only)
545
- if (hasLabels) {
546
- sendParams.config = { labels: mergedLabels };
547
- }
548
-
549
- const result = await this.chat.sendMessage(sendParams);
550
-
551
- // Capture and log response metadata for model verification and debugging
552
- this.lastResponseMetadata = {
553
- modelVersion: result.modelVersion || null,
554
- requestedModel: this.modelName,
555
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
556
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
557
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
558
- timestamp: Date.now()
559
- };
560
-
561
- if (result.usageMetadata && log.level !== 'silent') {
562
- log.debug(`API response metadata:`, {
563
- modelVersion: result.modelVersion || 'not-provided',
564
- requestedModel: this.modelName,
565
- promptTokens: result.usageMetadata.promptTokenCount,
566
- responseTokens: result.usageMetadata.candidatesTokenCount,
567
- totalTokens: result.usageMetadata.totalTokenCount
568
- });
569
- }
570
-
571
- const modelResponse = result.text;
572
- const extractedJSON = extractJSON(modelResponse); // Assuming extractJSON is defined
573
-
574
- // Unwrap the 'data' property if it exists
575
- if (extractedJSON?.data) {
576
- return extractedJSON.data;
577
- }
578
- return extractedJSON;
579
-
580
- } catch (error) {
581
- if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
582
- throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
583
- }
584
- // For other API errors, just re-throw
585
- throw new Error(`Transformation failed: ${error.message}`);
586
- }
587
- }
588
-
589
- /**
590
- * (Engine) Transforms a payload with validation and automatic retry logic.
591
- * @this {ExportedAPI}
592
- * @param {Object} sourcePayload - The source payload to transform.
593
- * @param {Object} [options] - Options for the validation process.
594
- * @param {AsyncValidatorFunction | null} validatorFn - The specific validator to use for this run.
595
- * @returns {Promise<Object>} - The validated transformed payload.
596
- */
597
- async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
598
- if (!this.chat) {
599
- throw new Error("Chat session not initialized. Please call init() first.");
600
- }
601
-
602
- // Handle stateless messages separately - they don't add to chat history
603
- if (options.stateless) {
604
- return await statelessMessage.call(this, sourcePayload, options, validatorFn);
605
- }
606
-
607
- const maxRetries = options.maxRetries ?? this.maxRetries;
608
- const retryDelay = options.retryDelay ?? this.retryDelay;
609
-
610
- // Check if grounding should be enabled for this specific message
611
- const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
612
- const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
613
-
614
- // Reinitialize chat if grounding settings changed for this message
615
- if (enableGroundingForMessage !== this.enableGrounding) {
616
- const originalGrounding = this.enableGrounding;
617
- const originalConfig = this.groundingConfig;
618
-
619
- try {
620
- // Temporarily change grounding settings
621
- this.enableGrounding = enableGroundingForMessage;
622
- this.groundingConfig = groundingConfigForMessage;
623
-
624
- // Force reinit with new settings
625
- await this.init(true);
626
-
627
- // Log the change
628
- if (enableGroundingForMessage) {
629
- log.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
630
- } else {
631
- log.debug(`Search grounding DISABLED for this message`);
632
- }
633
- } catch (error) {
634
- // Restore original settings on error
635
- this.enableGrounding = originalGrounding;
636
- this.groundingConfig = originalConfig;
637
- throw error;
638
- }
639
-
640
- // Schedule restoration after message completes
641
- const restoreGrounding = async () => {
642
- this.enableGrounding = originalGrounding;
643
- this.groundingConfig = originalConfig;
644
- await this.init(true);
645
- };
646
-
647
- // Store restoration function to call after message completes
648
- options._restoreGrounding = restoreGrounding;
649
- }
650
-
651
- let lastError = null;
652
- let lastPayload = null; // Store the payload that caused the validation error
653
-
654
- // Prepare the payload
655
- if (sourcePayload && isJSON(sourcePayload)) {
656
- lastPayload = JSON.stringify(sourcePayload, null, 2);
657
- } else if (typeof sourcePayload === 'string') {
658
- lastPayload = sourcePayload;
659
- }
660
- else if (typeof sourcePayload === 'boolean' || typeof sourcePayload === 'number') {
661
- lastPayload = sourcePayload.toString();
662
- }
663
- else if (sourcePayload === null || sourcePayload === undefined) {
664
- lastPayload = JSON.stringify({}); // Convert null/undefined to empty object
665
- }
666
- else {
667
- throw new Error("Invalid source payload. Must be a JSON object or string.");
668
- }
669
-
670
- // Extract per-message labels for passing to rawMessage
671
- const messageOptions = {};
672
- if (options.labels) {
673
- messageOptions.labels = options.labels;
674
- }
675
-
676
- // Reset cumulative usage tracking for this message call
677
- this._cumulativeUsage = {
678
- promptTokens: 0,
679
- responseTokens: 0,
680
- totalTokens: 0,
681
- attempts: 0
682
- };
683
-
684
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
685
- try {
686
- // Step 1: Get the transformed payload
687
- const transformedPayload = (attempt === 0)
688
- ? await this.rawMessage(lastPayload, messageOptions) // Use the new raw method with per-message options
689
- : await this.rebuild(lastPayload, lastError.message);
690
-
691
- // Accumulate token usage from this attempt
692
- if (this.lastResponseMetadata) {
693
- this._cumulativeUsage.promptTokens += this.lastResponseMetadata.promptTokens || 0;
694
- this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
695
- this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
696
- this._cumulativeUsage.attempts = attempt + 1;
697
- }
698
-
699
- lastPayload = transformedPayload; // Always update lastPayload *before* validation
700
-
701
- // Step 2: Validate if a validator is provided
702
- if (validatorFn) {
703
- await validatorFn(transformedPayload); // Validator throws on failure
704
- }
705
-
706
- // Step 3: Success!
707
- log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
708
-
709
- // Restore original grounding settings if they were changed
710
- if (options._restoreGrounding) {
711
- await options._restoreGrounding();
712
- }
713
-
714
- return transformedPayload;
715
-
716
- } catch (error) {
717
- lastError = error;
718
- log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
719
-
720
- if (attempt >= maxRetries) {
721
- log.error(`All ${maxRetries + 1} attempts failed.`)
722
- ;
723
- // Restore original grounding settings even on failure
724
- if (options._restoreGrounding) {
725
- await options._restoreGrounding();
726
- }
727
-
728
- throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
729
- }
730
-
731
- // Wait before retrying
732
- const delay = retryDelay * Math.pow(2, attempt);
733
- await new Promise(res => setTimeout(res, delay));
734
- }
735
- }
736
- }
737
-
738
- /**
739
- * Rebuilds a payload based on server error feedback
740
- * @this {ExportedAPI}
741
- * @param {Object} lastPayload - The payload that failed validation
742
- * @param {string} serverError - The error message from the server
743
- * @returns {Promise<Object>} - A new corrected payload
744
- * @throws {Error} If the rebuild process fails.
745
- */
746
- async function rebuildPayload(lastPayload, serverError) {
747
- await this.init(); // Ensure chat is initialized
748
- const prompt = `
749
- The previous JSON payload (below) failed validation.
750
- The server's error message is quoted afterward.
751
-
752
- ---------------- BAD PAYLOAD ----------------
753
- ${JSON.stringify(lastPayload, null, 2)}
754
-
755
-
756
- ---------------- SERVER ERROR ----------------
757
- ${serverError}
758
-
759
- Please return a NEW JSON payload that corrects the issue.
760
- Respond with JSON only – no comments or explanations.
761
- `;
762
-
763
- let result;
764
- try {
765
- result = await this.chat.sendMessage({ message: prompt });
766
-
767
- // Capture and log response metadata for rebuild calls too
768
- this.lastResponseMetadata = {
769
- modelVersion: result.modelVersion || null,
770
- requestedModel: this.modelName,
771
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
772
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
773
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
774
- timestamp: Date.now()
775
- };
776
-
777
- if (result.usageMetadata && log.level !== 'silent') {
778
- log.debug(`Rebuild response metadata - tokens used:`, result.usageMetadata.totalTokenCount);
779
- }
780
- } catch (err) {
781
- throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
782
- }
783
-
784
- try {
785
- const text = result.text ?? result.response ?? '';
786
- return typeof text === 'object' ? text : JSON.parse(text);
787
- } catch (parseErr) {
788
- throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
789
- }
790
- }
791
-
792
-
793
-
794
-
795
- /**
796
- * Estimate INPUT tokens only for a payload before sending.
797
- * This estimates the tokens that will be consumed by your prompt (input), NOT the response (output).
798
- * Includes: system instructions + chat history (seeded examples) + your new message.
799
- * Use this to preview input token costs and avoid exceeding context window limits.
2
+ * @fileoverview ak-gemini — Easy-to-use wrappers on @google/genai.
800
3
  *
801
- * NOTE: Output tokens cannot be predicted before the API call. Use getLastUsage() after
802
- * calling message() to see actual input + output token consumption.
803
- *
804
- * @this {ExportedAPI}
805
- * @param {object|string} nextPayload - The next user message to be sent (object or string)
806
- * @returns {Promise<{ inputTokens: number }>} - Estimated input token count
807
- */
808
- async function estimateInputTokens(nextPayload) {
809
- // Compose the conversation contents, Gemini-style
810
- const contents = [];
811
-
812
- // (1) System instructions (if applicable)
813
- if (this.systemInstructions) {
814
- // Add as a 'system' part; adjust role if Gemini supports
815
- contents.push({ parts: [{ text: this.systemInstructions }] });
816
- }
817
-
818
- // (2) All current chat history (seeded examples + real user/model turns)
819
- if (this.chat && typeof this.chat.getHistory === "function") {
820
- const history = this.chat.getHistory();
821
- if (Array.isArray(history) && history.length > 0) {
822
- contents.push(...history);
823
- }
824
- }
825
-
826
- // (3) The next user message
827
- const nextMessage = typeof nextPayload === "string"
828
- ? nextPayload
829
- : JSON.stringify(nextPayload, null, 2);
830
-
831
- contents.push({ parts: [{ text: nextMessage }] });
832
-
833
- // Call Gemini's token estimator
834
- const resp = await this.genAIClient.models.countTokens({
835
- model: this.modelName,
836
- contents,
837
- });
838
-
839
- // Return with clear naming - this is INPUT tokens only
840
- return { inputTokens: resp.totalTokens };
841
- }
842
-
843
- // Model pricing per million tokens (as of Dec 2025)
844
- // https://ai.google.dev/gemini-api/docs/pricing
845
- const MODEL_PRICING = {
846
- 'gemini-2.5-flash': { input: 0.15, output: 0.60 },
847
- 'gemini-2.5-flash-lite': { input: 0.02, output: 0.10 },
848
- 'gemini-2.5-pro': { input: 2.50, output: 10.00 },
849
- 'gemini-3-pro': { input: 2.00, output: 12.00 },
850
- 'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
851
- 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
852
- 'gemini-2.0-flash-lite': { input: 0.02, output: 0.10 }
853
- };
854
-
855
- /**
856
- * Estimates the cost of sending a payload based on input token count and model pricing.
857
- * NOTE: This only estimates INPUT cost. Output cost depends on response length and cannot be predicted.
858
- * @this {ExportedAPI}
859
- * @param {object|string} nextPayload - The next user message to be sent (object or string)
860
- * @returns {Promise<Object>} - Cost estimation including input tokens, model, pricing, and estimated input cost
861
- */
862
- async function estimateCost(nextPayload) {
863
- const tokenInfo = await this.estimate(nextPayload);
864
- const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
865
-
866
- return {
867
- inputTokens: tokenInfo.inputTokens,
868
- model: this.modelName,
869
- pricing: pricing,
870
- estimatedInputCost: (tokenInfo.inputTokens / 1_000_000) * pricing.input,
871
- note: 'Cost is for input tokens only; output cost depends on response length'
872
- };
873
- }
874
-
875
-
876
- /**
877
- * Resets the current chat session, clearing all history and examples
878
- * @this {ExportedAPI}
879
- * @returns {Promise<void>}
880
- */
881
- async function resetChat() {
882
- if (this.chat) {
883
- log.debug("Resetting Gemini chat session...");
884
-
885
- // Prepare chat options with grounding if enabled
886
- const chatOptions = {
887
- model: this.modelName,
888
- // @ts-ignore
889
- config: {
890
- ...this.chatConfig,
891
- ...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
892
- },
893
- history: [],
894
- };
895
-
896
- // Only add tools if grounding is explicitly enabled
897
- if (this.enableGrounding) {
898
- chatOptions.config.tools = [{
899
- googleSearch: this.groundingConfig
900
- }];
901
- log.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
902
- }
903
-
904
- this.chat = await this.genAIClient.chats.create(chatOptions);
905
- log.debug("Chat session reset.");
906
- } else {
907
- log.warn("Cannot reset chat session: chat not yet initialized.");
908
- }
909
- }
910
-
911
- /**
912
- * Retrieves the current conversation history for debugging or inspection
913
- * @returns {Array<Object>} - An array of message objects in the conversation.
914
- */
915
- function getChatHistory() {
916
- if (!this.chat) {
917
- log.warn("Chat session not initialized. No history available.");
918
- return [];
919
- }
920
- return this.chat.getHistory();
921
- }
922
-
923
- /**
924
- * Updates system instructions and reinitializes the chat session
925
- * @this {ExportedAPI}
926
- * @param {string} newInstructions - The new system instructions
927
- * @returns {Promise<void>}
928
- */
929
- async function updateSystemInstructions(newInstructions) {
930
- if (!newInstructions || typeof newInstructions !== 'string') {
931
- throw new Error('System instructions must be a non-empty string');
932
- }
933
-
934
- this.systemInstructions = newInstructions.trim();
935
- this.chatConfig.systemInstruction = this.systemInstructions;
936
-
937
- log.debug('Updating system instructions and reinitializing chat...');
938
- await this.init(true); // Force reinitialize with new instructions
939
- }
940
-
941
- /**
942
- * Clears conversation history while preserving seeded examples.
943
- * Useful for starting a fresh conversation within the same session
944
- * without losing the few-shot learning examples.
945
- * @this {ExportedAPI}
946
- * @returns {Promise<void>}
947
- */
948
- async function clearConversation() {
949
- if (!this.chat) {
950
- log.warn("Cannot clear conversation: chat not initialized.");
951
- return;
952
- }
953
-
954
- const history = this.chat.getHistory();
955
- const exampleHistory = history.slice(0, this.exampleCount || 0);
956
-
957
- this.chat = await this.genAIClient.chats.create({
958
- model: this.modelName,
959
- // @ts-ignore
960
- config: {
961
- ...this.chatConfig,
962
- ...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
963
- },
964
- history: exampleHistory,
965
- });
966
-
967
- // Reset usage tracking for the new conversation
968
- this.lastResponseMetadata = null;
969
- this._cumulativeUsage = {
970
- promptTokens: 0,
971
- responseTokens: 0,
972
- totalTokens: 0,
973
- attempts: 0
974
- };
975
-
976
- log.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
977
- }
978
-
979
- /**
980
- * Returns structured usage data from the last message call for billing verification.
981
- * Includes CUMULATIVE token counts across all retry attempts.
982
- * Call this after message() or statelessMessage() to get actual token consumption.
4
+ * Exports:
5
+ * - Transformer AI-powered JSON transformation via few-shot learning
6
+ * - Chat — Multi-turn text conversation with AI
7
+ * - Message — Stateless one-off messages to AI
8
+ * - ToolAgent AI agent with user-provided tools
9
+ * - CodeAgent AI agent that writes and executes code (stub)
10
+ * - BaseGemini — Base class for building custom wrappers
983
11
  *
984
- * @this {ExportedAPI}
985
- * @returns {Object|null} Usage data with promptTokens, responseTokens, totalTokens, attempts, etc.
986
- * Returns null if no API call has been made yet.
987
- */
988
- function getLastUsage() {
989
- if (!this.lastResponseMetadata) {
990
- return null;
991
- }
992
-
993
- const meta = this.lastResponseMetadata;
994
- const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
995
-
996
- // Use cumulative tokens if tracking was active (attempts > 0), otherwise fall back to last response
997
- const useCumulative = cumulative.attempts > 0;
998
-
999
- return {
1000
- // Token breakdown for billing - CUMULATIVE across all retry attempts
1001
- promptTokens: useCumulative ? cumulative.promptTokens : meta.promptTokens,
1002
- responseTokens: useCumulative ? cumulative.responseTokens : meta.responseTokens,
1003
- totalTokens: useCumulative ? cumulative.totalTokens : meta.totalTokens,
1004
-
1005
- // Number of attempts (1 = success on first try, 2+ = retries were needed)
1006
- attempts: useCumulative ? cumulative.attempts : 1,
1007
-
1008
- // Model verification for billing cross-check
1009
- modelVersion: meta.modelVersion, // Actual model that responded (e.g., 'gemini-2.5-flash-001')
1010
- requestedModel: meta.requestedModel, // Model you requested (e.g., 'gemini-2.5-flash')
1011
-
1012
- // Timestamp for audit trail
1013
- timestamp: meta.timestamp
1014
- };
1015
- }
1016
-
1017
- /**
1018
- * Sends a one-off message using generateContent (not chat).
1019
- * Does NOT affect chat history - useful for isolated requests.
1020
- * @this {ExportedAPI}
1021
- * @param {Object|string} sourcePayload - The source payload.
1022
- * @param {Object} [options] - Options including labels.
1023
- * @param {AsyncValidatorFunction|null} [validatorFn] - Optional validator.
1024
- * @returns {Promise<Object>} - The transformed payload.
1025
- */
1026
- async function statelessMessage(sourcePayload, options = {}, validatorFn = null) {
1027
- if (!this.chat) {
1028
- throw new Error("Chat session not initialized. Please call init() first.");
1029
- }
1030
-
1031
- const payloadStr = typeof sourcePayload === 'string'
1032
- ? sourcePayload
1033
- : JSON.stringify(sourcePayload, null, 2);
1034
-
1035
- // Build contents including examples from current chat history
1036
- const contents = [];
1037
-
1038
- // Include seeded examples if we have them
1039
- if (this.exampleCount > 0) {
1040
- const history = this.chat.getHistory();
1041
- const exampleHistory = history.slice(0, this.exampleCount);
1042
- contents.push(...exampleHistory);
1043
- }
1044
-
1045
- // Add the user message
1046
- contents.push({ role: 'user', parts: [{ text: payloadStr }] });
1047
-
1048
- // Merge labels (Vertex AI only)
1049
- const mergedLabels = { ...this.labels, ...(options.labels || {}) };
1050
-
1051
- // Use generateContent instead of chat.sendMessage
1052
- const result = await this.genAIClient.models.generateContent({
1053
- model: this.modelName,
1054
- contents: contents,
1055
- config: {
1056
- ...this.chatConfig,
1057
- ...(this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels })
1058
- }
1059
- });
1060
-
1061
- // Capture and log response metadata
1062
- this.lastResponseMetadata = {
1063
- modelVersion: result.modelVersion || null,
1064
- requestedModel: this.modelName,
1065
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
1066
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
1067
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
1068
- timestamp: Date.now()
1069
- };
1070
-
1071
- // Set cumulative usage for stateless message (single attempt, no retries)
1072
- this._cumulativeUsage = {
1073
- promptTokens: this.lastResponseMetadata.promptTokens,
1074
- responseTokens: this.lastResponseMetadata.responseTokens,
1075
- totalTokens: this.lastResponseMetadata.totalTokens,
1076
- attempts: 1
1077
- };
1078
-
1079
- if (result.usageMetadata && log.level !== 'silent') {
1080
- log.debug(`Stateless message metadata:`, {
1081
- modelVersion: result.modelVersion || 'not-provided',
1082
- promptTokens: result.usageMetadata.promptTokenCount,
1083
- responseTokens: result.usageMetadata.candidatesTokenCount
1084
- });
1085
- }
1086
-
1087
- const modelResponse = result.text;
1088
- const extractedJSON = extractJSON(modelResponse);
1089
-
1090
- let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
1091
-
1092
- // Validate if a validator is provided
1093
- if (validatorFn) {
1094
- await validatorFn(transformedPayload);
1095
- }
1096
-
1097
- return transformedPayload;
1098
- }
1099
-
1100
-
1101
- /*
1102
- ----
1103
- HELPERS
1104
- ----
1105
- */
1106
-
1107
- /**
1108
- * Attempts to recover truncated JSON by progressively removing characters from the end
1109
- * until valid JSON is found or recovery fails
1110
- * @param {string} text - The potentially truncated JSON string
1111
- * @param {number} maxAttempts - Maximum number of characters to remove
1112
- * @returns {Object|null} - Parsed JSON object or null if recovery fails
1113
- */
1114
- function attemptJSONRecovery(text, maxAttempts = 100) {
1115
- if (!text || typeof text !== 'string') return null;
1116
-
1117
- // First, try parsing as-is
1118
- try {
1119
- return JSON.parse(text);
1120
- } catch (e) {
1121
- // Continue with recovery
1122
- }
1123
-
1124
- let workingText = text.trim();
1125
-
1126
- // First attempt: try to close unclosed structures without removing characters
1127
- // Count open/close braces and brackets in the original text
1128
- let braces = 0;
1129
- let brackets = 0;
1130
- let inString = false;
1131
- let escapeNext = false;
1132
-
1133
- for (let j = 0; j < workingText.length; j++) {
1134
- const char = workingText[j];
1135
-
1136
- if (escapeNext) {
1137
- escapeNext = false;
1138
- continue;
1139
- }
1140
-
1141
- if (char === '\\') {
1142
- escapeNext = true;
1143
- continue;
1144
- }
1145
-
1146
- if (char === '"') {
1147
- inString = !inString;
1148
- continue;
1149
- }
1150
-
1151
- if (!inString) {
1152
- if (char === '{') braces++;
1153
- else if (char === '}') braces--;
1154
- else if (char === '[') brackets++;
1155
- else if (char === ']') brackets--;
1156
- }
1157
- }
1158
-
1159
- // Try to fix by just adding closing characters
1160
- if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
1161
- let fixedText = workingText;
1162
-
1163
- // Close any open strings first
1164
- if (inString) {
1165
- fixedText += '"';
1166
- }
1167
-
1168
- // Add missing closing characters
1169
- while (braces > 0) {
1170
- fixedText += '}';
1171
- braces--;
1172
- }
1173
- while (brackets > 0) {
1174
- fixedText += ']';
1175
- brackets--;
1176
- }
1177
-
1178
- try {
1179
- const result = JSON.parse(fixedText);
1180
- if (log.level !== 'silent') {
1181
- log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
1182
- }
1183
- return result;
1184
- } catch (e) {
1185
- // Simple fix didn't work, continue with more aggressive recovery
1186
- }
1187
- }
1188
-
1189
- // Second attempt: progressively remove characters from the end
1190
-
1191
- for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
1192
- // Remove one character from the end
1193
- workingText = workingText.slice(0, -1);
1194
-
1195
- // Count open/close braces and brackets
1196
- let braces = 0;
1197
- let brackets = 0;
1198
- let inString = false;
1199
- let escapeNext = false;
1200
-
1201
- for (let j = 0; j < workingText.length; j++) {
1202
- const char = workingText[j];
1203
-
1204
- if (escapeNext) {
1205
- escapeNext = false;
1206
- continue;
1207
- }
1208
-
1209
- if (char === '\\') {
1210
- escapeNext = true;
1211
- continue;
1212
- }
1213
-
1214
- if (char === '"') {
1215
- inString = !inString;
1216
- continue;
1217
- }
1218
-
1219
- if (!inString) {
1220
- if (char === '{') braces++;
1221
- else if (char === '}') braces--;
1222
- else if (char === '[') brackets++;
1223
- else if (char === ']') brackets--;
1224
- }
1225
- }
1226
-
1227
- // If we have balanced braces/brackets, try parsing
1228
- if (braces === 0 && brackets === 0 && !inString) {
1229
- try {
1230
- const result = JSON.parse(workingText);
1231
- if (log.level !== 'silent') {
1232
- log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
1233
- }
1234
- return result;
1235
- } catch (e) {
1236
- // Continue trying
1237
- }
1238
- }
1239
-
1240
- // After a few attempts, try adding closing characters
1241
- if (i > 5) {
1242
- let fixedText = workingText;
1243
-
1244
- // Close any open strings first
1245
- if (inString) {
1246
- fixedText += '"';
1247
- }
1248
-
1249
- // Add missing closing characters
1250
- while (braces > 0) {
1251
- fixedText += '}';
1252
- braces--;
1253
- }
1254
- while (brackets > 0) {
1255
- fixedText += ']';
1256
- brackets--;
1257
- }
1258
-
1259
- try {
1260
- const result = JSON.parse(fixedText);
1261
- if (log.level !== 'silent') {
1262
- log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
1263
- }
1264
- return result;
1265
- } catch (e) {
1266
- // Recovery failed, continue trying
1267
- }
1268
- }
1269
- }
1270
-
1271
- return null;
1272
- }
1273
-
1274
- function isJSON(data) {
1275
- try {
1276
- const attempt = JSON.stringify(data);
1277
- if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
1278
- if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
1279
- return true;
1280
- }
1281
- }
1282
- return false;
1283
- } catch (e) {
1284
- return false;
1285
- }
1286
- }
1287
-
1288
- function isJSONStr(string) {
1289
- if (typeof string !== 'string') return false;
1290
- try {
1291
- const result = JSON.parse(string);
1292
- const type = Object.prototype.toString.call(result);
1293
- return type === '[object Object]' || type === '[object Array]';
1294
- } catch (err) {
1295
- return false;
1296
- }
1297
- }
1298
-
1299
- function extractJSON(text) {
1300
- if (!text || typeof text !== 'string') {
1301
- throw new Error('No text provided for JSON extraction');
1302
- }
1303
-
1304
- // Strategy 1: Try parsing the entire response as JSON
1305
- if (isJSONStr(text.trim())) {
1306
- return JSON.parse(text.trim());
1307
- }
1308
-
1309
- // Strategy 2: Look for JSON code blocks (```json...``` or ```...```)
1310
- const codeBlockPatterns = [
1311
- /```json\s*\n?([\s\S]*?)\n?\s*```/gi,
1312
- /```\s*\n?([\s\S]*?)\n?\s*```/gi
1313
- ];
1314
-
1315
- for (const pattern of codeBlockPatterns) {
1316
- const matches = text.match(pattern);
1317
- if (matches) {
1318
- for (const match of matches) {
1319
- const jsonContent = match.replace(/```json\s*\n?/gi, '').replace(/```\s*\n?/gi, '').trim();
1320
- if (isJSONStr(jsonContent)) {
1321
- return JSON.parse(jsonContent);
1322
- }
1323
- }
1324
- }
1325
- }
1326
-
1327
- // Strategy 3: Look for JSON objects/arrays using bracket matching
1328
- const jsonPatterns = [
1329
- // Match complete JSON objects
1330
- /\{[\s\S]*\}/g,
1331
- // Match complete JSON arrays
1332
- /\[[\s\S]*\]/g
1333
- ];
1334
-
1335
- for (const pattern of jsonPatterns) {
1336
- const matches = text.match(pattern);
1337
- if (matches) {
1338
- for (const match of matches) {
1339
- const candidate = match.trim();
1340
- if (isJSONStr(candidate)) {
1341
- return JSON.parse(candidate);
1342
- }
1343
- }
1344
- }
1345
- }
1346
-
1347
- // Strategy 4: Advanced bracket matching for nested structures
1348
- const advancedExtract = findCompleteJSONStructures(text);
1349
- if (advancedExtract.length > 0) {
1350
- // Return the first valid JSON structure found
1351
- for (const candidate of advancedExtract) {
1352
- if (isJSONStr(candidate)) {
1353
- return JSON.parse(candidate);
1354
- }
1355
- }
1356
- }
1357
-
1358
- // Strategy 5: Clean up common formatting issues and retry
1359
- const cleanedText = text
1360
- .replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, '') // Remove conversational intros
1361
- .replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, '')
1362
- .replace(/^\s*The\s+.*?is\s*[:\n]/gi, '')
1363
- .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* comments */
1364
- .replace(/\/\/.*$/gm, '') // Remove // comments
1365
- .trim();
1366
-
1367
- if (isJSONStr(cleanedText)) {
1368
- return JSON.parse(cleanedText);
1369
- }
1370
-
1371
- // Strategy 6: Last resort - attempt recovery for potentially truncated JSON
1372
- // This is especially useful when maxOutputTokens might have cut off the response
1373
- const recoveredJSON = attemptJSONRecovery(text);
1374
- if (recoveredJSON !== null) {
1375
- return recoveredJSON;
1376
- }
1377
-
1378
- // If all else fails, throw an error with helpful information
1379
- throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
1380
- }
1381
-
1382
- function findCompleteJSONStructures(text) {
1383
- const results = [];
1384
- const startChars = ['{', '['];
1385
-
1386
- for (let i = 0; i < text.length; i++) {
1387
- if (startChars.includes(text[i])) {
1388
- const extracted = extractCompleteStructure(text, i);
1389
- if (extracted) {
1390
- results.push(extracted);
1391
- }
1392
- }
1393
- }
1394
-
1395
- return results;
1396
- }
1397
-
1398
-
1399
- function extractCompleteStructure(text, startPos) {
1400
- const startChar = text[startPos];
1401
- const endChar = startChar === '{' ? '}' : ']';
1402
- let depth = 0;
1403
- let inString = false;
1404
- let escaped = false;
1405
-
1406
- for (let i = startPos; i < text.length; i++) {
1407
- const char = text[i];
1408
-
1409
- if (escaped) {
1410
- escaped = false;
1411
- continue;
1412
- }
1413
-
1414
- if (char === '\\' && inString) {
1415
- escaped = true;
1416
- continue;
1417
- }
1418
-
1419
- if (char === '"' && !escaped) {
1420
- inString = !inString;
1421
- continue;
1422
- }
1423
-
1424
- if (!inString) {
1425
- if (char === startChar) {
1426
- depth++;
1427
- } else if (char === endChar) {
1428
- depth--;
1429
- if (depth === 0) {
1430
- return text.substring(startPos, i + 1);
1431
- }
1432
- }
1433
- }
1434
- }
1435
-
1436
- return null; // Incomplete structure
1437
- }
1438
-
1439
- if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
1440
- log.info("RUNNING AI Transformer as standalone script...");
1441
- (
1442
- async () => {
1443
- try {
1444
- log.info("Initializing AI Transformer...");
1445
- const transformer = new AITransformer({
1446
- modelName: 'gemini-2.5-flash',
1447
- sourceKey: 'INPUT', // Custom source key
1448
- targetKey: 'OUTPUT', // Custom target key
1449
- contextKey: 'CONTEXT', // Custom context key
1450
- maxRetries: 2,
1451
-
1452
- });
1453
-
1454
- const examples = [
1455
- {
1456
- CONTEXT: "Generate professional profiles with emoji representations",
1457
- INPUT: { "name": "Alice" },
1458
- OUTPUT: { "name": "Alice", "profession": "data scientist", "life_as_told_by_emoji": ["🔬", "💡", "📊", "🧠", "🌟"] }
1459
- },
1460
- {
1461
- INPUT: { "name": "Bob" },
1462
- OUTPUT: { "name": "Bob", "profession": "product manager", "life_as_told_by_emoji": ["📋", "🤝", "🚀", "💬", "🎯"] }
1463
- },
1464
- {
1465
- INPUT: { "name": "Eve" },
1466
- OUTPUT: { "name": "Even", "profession": "security analyst", "life_as_told_by_emoji": ["🕵️‍♀️", "🔒", "💻", "👀", "⚡️"] }
1467
- },
1468
- ];
1469
-
1470
- await transformer.init();
1471
- await transformer.seed(examples);
1472
- log.info("AI Transformer initialized and seeded with examples.");
1473
-
1474
- // Test normal transformation
1475
- const normalResponse = await transformer.message({ "name": "AK" });
1476
- log.info("Normal Payload Transformed", normalResponse);
1477
-
1478
- // Test transformation with validation
1479
- const mockValidator = async (payload) => {
1480
- // Simulate validation logic
1481
- if (!payload.profession || !payload.life_as_told_by_emoji) {
1482
- throw new Error("Missing required fields: profession or life_as_told_by_emoji");
1483
- }
1484
- if (!Array.isArray(payload.life_as_told_by_emoji)) {
1485
- throw new Error("life_as_told_by_emoji must be an array");
1486
- }
1487
- return payload; // Return the payload if validation passes
1488
- };
1489
-
1490
- const validatedResponse = await transformer.messageAndValidate(
1491
- { "name": "Lynn" },
1492
- {},
1493
- mockValidator
1494
- );
1495
- log.info("Validated Payload Transformed", validatedResponse);
1496
-
1497
- if (NODE_ENV === 'dev') debugger;
1498
- } catch (error) {
1499
- log.error("Error in AI Transformer script:", error);
1500
- if (NODE_ENV === 'dev') debugger;
1501
- }
1502
- })();
1503
- }
12
+ * @example
13
+ * ```javascript
14
+ * import { Transformer, Chat, Message, ToolAgent } from 'ak-gemini';
15
+ * // or
16
+ * import AI from 'ak-gemini';
17
+ * const t = new AI.Transformer({ ... });
18
+ * ```
19
+ */
20
+
21
+ // ── Named Exports ──
22
+
23
+ export { default as Transformer } from './transformer.js';
24
+ export { default as Chat } from './chat.js';
25
+ export { default as Message } from './message.js';
26
+ export { default as ToolAgent } from './tool-agent.js';
27
+ export { default as CodeAgent } from './code-agent.js';
28
+ export { default as BaseGemini } from './base.js';
29
+ export { default as log } from './logger.js';
30
+ export { ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
31
+ export { extractJSON, attemptJSONRecovery } from './json-helpers.js';
32
+
33
+ // ── Default Export (namespace object) ──
34
+
35
+ import Transformer from './transformer.js';
36
+ import Chat from './chat.js';
37
+ import Message from './message.js';
38
+ import ToolAgent from './tool-agent.js';
39
+ import CodeAgent from './code-agent.js';
40
+
41
+ export default { Transformer, Chat, Message, ToolAgent, CodeAgent };