ak-gemini 1.0.5 → 1.0.7

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.
Files changed (6) hide show
  1. package/README.md +4 -3
  2. package/index.cjs +316 -86
  3. package/index.js +458 -126
  4. package/package.json +23 -17
  5. package/types.d.ts +138 -0
  6. package/types.ts +0 -65
package/index.js CHANGED
@@ -18,20 +18,17 @@
18
18
  //env
19
19
  import dotenv from 'dotenv';
20
20
  dotenv.config();
21
- const { NODE_ENV = "unknown", GEMINI_API_KEY } = process.env;
21
+ const { NODE_ENV = "unknown", GEMINI_API_KEY, LOG_LEVEL = "" } = process.env;
22
22
 
23
23
 
24
24
 
25
25
  //deps
26
- import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
26
+ import { GoogleGenAI, HarmCategory, HarmBlockThreshold, ThinkingLevel } from '@google/genai';
27
27
  import u from 'ak-tools';
28
28
  import path from 'path';
29
29
  import log from './logger.js';
30
30
  export { log };
31
-
32
- if (NODE_ENV === 'dev') log.level = 'debug';
33
- if (NODE_ENV === 'test') log.level = 'warn';
34
- if (NODE_ENV.startsWith('prod')) log.level = 'error';
31
+ export { ThinkingLevel };
35
32
 
36
33
 
37
34
 
@@ -44,7 +41,7 @@ const DEFAULT_SAFETY_SETTINGS = [
44
41
  const DEFAULT_SYSTEM_INSTRUCTIONS = `
45
42
  You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
46
43
 
47
- You will be provided with example transformations (Source JSON -> Target JSON).
44
+ You will be provided with example transformations (Source JSON -> Target JSON).
48
45
 
49
46
  Learn the mapping rules from these examples.
50
47
 
@@ -55,6 +52,22 @@ Always respond ONLY with a valid JSON object that strictly adheres to the expect
55
52
  Do not include any additional text, explanations, or formatting before or after the JSON object.
56
53
  `;
57
54
 
55
+ const DEFAULT_THINKING_CONFIG = {
56
+ thinkingBudget: 0,
57
+ thinkingLevel: ThinkingLevel.MINIMAL
58
+ };
59
+
60
+ // Models that support thinking features (as of Dec 2024)
61
+ // Using regex patterns for more precise matching
62
+ const THINKING_SUPPORTED_MODELS = [
63
+ /^gemini-3-flash(-preview)?$/,
64
+ /^gemini-3-pro(-preview|-image-preview)?$/,
65
+ /^gemini-2\.5-pro/,
66
+ /^gemini-2\.5-flash(-preview)?$/,
67
+ /^gemini-2\.5-flash-lite(-preview)?$/,
68
+ /^gemini-2\.0-flash$/ // Experimental support, exact match only
69
+ ];
70
+
58
71
  const DEFAULT_CHAT_CONFIG = {
59
72
  responseMimeType: 'application/json',
60
73
  temperature: 0.2,
@@ -64,14 +77,20 @@ const DEFAULT_CHAT_CONFIG = {
64
77
  safetySettings: DEFAULT_SAFETY_SETTINGS
65
78
  };
66
79
 
80
+ /**
81
+ * @typedef {import('./types').AITransformer} AITransformerUtility
82
+ */
83
+
84
+
85
+
67
86
  /**
68
87
  * main export class for AI Transformer
69
88
  * @class AITransformer
89
+ * @type {AITransformerUtility}
70
90
  * @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
71
91
  * @implements {ExportedAPI}
72
92
  */
73
- // @ts-ignore
74
- export default class AITransformer {
93
+ class AITransformer {
75
94
  /**
76
95
  * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
77
96
  *
@@ -81,25 +100,43 @@ export default class AITransformer {
81
100
  this.promptKey = "";
82
101
  this.answerKey = "";
83
102
  this.contextKey = "";
103
+ this.explanationKey = "";
104
+ this.systemInstructionKey = "";
84
105
  this.maxRetries = 3;
85
106
  this.retryDelay = 1000;
86
107
  this.systemInstructions = "";
87
108
  this.chatConfig = {};
88
109
  this.apiKey = GEMINI_API_KEY;
110
+ this.onlyJSON = true; // always return JSON
111
+ this.asyncValidator = null; // for transformWithValidation
112
+ this.logLevel = 'info'; // default log level
89
113
  AITransformFactory.call(this, options);
90
114
 
91
115
  //external API
92
116
  this.init = initChat.bind(this);
93
117
  this.seed = seedWithExamples.bind(this);
94
- this.message = transformJSON.bind(this);
118
+
119
+ // Internal "raw" message sender
120
+ this.rawMessage = rawMessage.bind(this);
121
+
122
+ // The public `.message()` method uses the GLOBAL validator
123
+ this.message = (payload, opts = {}, validatorFn = null) => {
124
+
125
+ return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
126
+ };
127
+
95
128
  this.rebuild = rebuildPayload.bind(this);
96
129
  this.reset = resetChat.bind(this);
97
130
  this.getHistory = getChatHistory.bind(this);
98
- this.transformWithValidation = transformWithValidation.bind(this);
131
+ this.messageAndValidate = prepareAndValidateMessage.bind(this);
132
+ this.transformWithValidation = prepareAndValidateMessage.bind(this);
99
133
  this.estimate = estimateTokenUsage.bind(this);
134
+ this.estimateTokenUsage = estimateTokenUsage.bind(this);
100
135
  }
101
136
  }
102
137
 
138
+ export default AITransformer;
139
+
103
140
  /**
104
141
  * factory function to create an AI Transformer instance
105
142
  * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
@@ -107,11 +144,41 @@ export default class AITransformer {
107
144
  */
108
145
  function AITransformFactory(options = {}) {
109
146
  // ? https://ai.google.dev/gemini-api/docs/models
110
- this.modelName = options.modelName || 'gemini-2.0-flash';
147
+ this.modelName = options.modelName || 'gemini-2.5-flash';
111
148
  this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
112
149
 
113
- this.apiKey = options.apiKey || GEMINI_API_KEY;
150
+ // Configure log level - priority: options.logLevel > LOG_LEVEL env > NODE_ENV based defaults > 'info'
151
+ if (options.logLevel) {
152
+ this.logLevel = options.logLevel;
153
+ if (this.logLevel === 'none') {
154
+ // Set to silent to disable all logging
155
+ log.level = 'silent';
156
+ } else {
157
+ // Set the log level as specified
158
+ log.level = this.logLevel;
159
+ }
160
+ } else if (LOG_LEVEL) {
161
+ // Use environment variable if no option specified
162
+ this.logLevel = LOG_LEVEL;
163
+ log.level = LOG_LEVEL;
164
+ } else if (NODE_ENV === 'dev') {
165
+ this.logLevel = 'debug';
166
+ log.level = 'debug';
167
+ } else if (NODE_ENV === 'test') {
168
+ this.logLevel = 'warn';
169
+ log.level = 'warn';
170
+ } else if (NODE_ENV.startsWith('prod')) {
171
+ this.logLevel = 'error';
172
+ log.level = 'error';
173
+ } else {
174
+ // Default to info
175
+ this.logLevel = 'info';
176
+ log.level = 'info';
177
+ }
178
+
179
+ this.apiKey = options.apiKey !== undefined && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
114
180
  if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
181
+
115
182
  // Build chat config, making sure systemInstruction uses the custom instructions
116
183
  this.chatConfig = {
117
184
  ...DEFAULT_CHAT_CONFIG,
@@ -119,6 +186,28 @@ function AITransformFactory(options = {}) {
119
186
  systemInstruction: this.systemInstructions
120
187
  };
121
188
 
189
+ // Only add thinkingConfig if the model supports it
190
+ const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(pattern =>
191
+ pattern.test(this.modelName)
192
+ );
193
+
194
+ if (modelSupportsThinking && options.thinkingConfig) {
195
+ // Handle thinkingConfig - merge with defaults
196
+ const thinkingConfig = {
197
+ ...DEFAULT_THINKING_CONFIG,
198
+ ...options.thinkingConfig
199
+ };
200
+ this.chatConfig.thinkingConfig = thinkingConfig;
201
+
202
+ if (log.level !== 'silent') {
203
+ log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
204
+ }
205
+ } else if (options.thinkingConfig && !modelSupportsThinking) {
206
+ if (log.level !== 'silent') {
207
+ log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
208
+ }
209
+ }
210
+
122
211
  // response schema is optional, but if provided, it should be a valid JSON schema
123
212
  if (options.responseSchema) {
124
213
  this.chatConfig.responseSchema = options.responseSchema;
@@ -129,20 +218,30 @@ function AITransformFactory(options = {}) {
129
218
  this.exampleData = options.exampleData || null; // can be used instead of examplesFile
130
219
 
131
220
  // Use configurable keys with fallbacks
132
- this.promptKey = options.sourceKey || 'PROMPT';
133
- this.answerKey = options.targetKey || 'ANSWER';
134
- this.contextKey = options.contextKey || 'CONTEXT'; // Now configurable
221
+ this.promptKey = options.promptKey || options.sourceKey || 'PROMPT';
222
+ this.answerKey = options.answerKey || options.targetKey || 'ANSWER';
223
+ this.contextKey = options.contextKey || 'CONTEXT'; // Optional key for context
224
+ this.explanationKey = options.explanationKey || 'EXPLANATION'; // Optional key for explanations
225
+ this.systemInstructionsKey = options.systemInstructionsKey || 'SYSTEM'; // Optional key for system instructions
135
226
 
136
227
  // Retry configuration
137
228
  this.maxRetries = options.maxRetries || 3;
138
229
  this.retryDelay = options.retryDelay || 1000;
139
230
 
231
+ //allow async validation function
232
+ this.asyncValidator = options.asyncValidator || null; // Function to validate transformed payloads
233
+
234
+ //are we forcing json responses only?
235
+ this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
236
+
140
237
  if (this.promptKey === this.answerKey) {
141
238
  throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
142
239
  }
143
240
 
144
- log.debug(`Creating AI Transformer with model: ${this.modelName}`);
145
- log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
241
+ if (log.level !== 'silent') {
242
+ log.debug(`Creating AI Transformer with model: ${this.modelName}`);
243
+ log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
244
+ }
146
245
 
147
246
  const ai = new GoogleGenAI({ apiKey: this.apiKey });
148
247
  this.genAIClient = ai;
@@ -151,11 +250,12 @@ function AITransformFactory(options = {}) {
151
250
 
152
251
  /**
153
252
  * Initializes the chat session with the specified model and configurations.
253
+ * @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
154
254
  * @this {ExportedAPI}
155
255
  * @returns {Promise<void>}
156
256
  */
157
- async function initChat() {
158
- if (this.chat) return;
257
+ async function initChat(force = false) {
258
+ if (this.chat && !force) return;
159
259
 
160
260
  log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
161
261
 
@@ -166,6 +266,15 @@ async function initChat() {
166
266
  history: [],
167
267
  });
168
268
 
269
+ try {
270
+ await this.genAIClient.models.list();
271
+ log.debug("Gemini API connection successful.");
272
+ } catch (e) {
273
+ throw new Error(`Gemini chat initialization failed: ${e.message}`);
274
+ }
275
+
276
+
277
+
169
278
  log.debug("Gemini chat session initialized.");
170
279
  }
171
280
 
@@ -173,6 +282,7 @@ async function initChat() {
173
282
  * Seeds the chat session with example transformations.
174
283
  * @this {ExportedAPI}
175
284
  * @param {TransformationExample[]} [examples] - An array of transformation examples.
285
+ * @this {ExportedAPI}
176
286
  * @returns {Promise<void>}
177
287
  */
178
288
  async function seedWithExamples(examples) {
@@ -181,13 +291,38 @@ async function seedWithExamples(examples) {
181
291
  if (!examples || !Array.isArray(examples) || examples.length === 0) {
182
292
  if (this.examplesFile) {
183
293
  log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
184
- examples = await u.load(path.resolve(this.examplesFile), true);
185
- } else {
294
+ try {
295
+ // @ts-ignore
296
+ examples = await u.load(path.resolve(this.examplesFile), true);
297
+ }
298
+ catch (err) {
299
+ throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
300
+ }
301
+ }
302
+
303
+ else if (this.exampleData) {
304
+ log.debug(`Using example data provided in options.`);
305
+ if (Array.isArray(this.exampleData)) {
306
+ examples = this.exampleData;
307
+ } else {
308
+ throw new Error(`Invalid example data provided. Expected an array of examples.`);
309
+ }
310
+ }
311
+
312
+ else {
186
313
  log.debug("No examples provided and no examples file specified. Skipping seeding.");
187
314
  return;
188
315
  }
189
316
  }
190
317
 
318
+ const instructionExample = examples.find(ex => ex[this.systemInstructionsKey]);
319
+ if (instructionExample) {
320
+ log.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
321
+ this.systemInstructions = instructionExample[this.systemInstructionsKey];
322
+ this.chatConfig.systemInstruction = this.systemInstructions;
323
+ await this.init(true); // Reinitialize chat with new system instructions
324
+ }
325
+
191
326
  log.debug(`Seeding chat with ${examples.length} transformation examples...`);
192
327
  const historyToAdd = [];
193
328
 
@@ -196,35 +331,36 @@ async function seedWithExamples(examples) {
196
331
  const contextValue = example[this.contextKey] || "";
197
332
  const promptValue = example[this.promptKey] || "";
198
333
  const answerValue = example[this.answerKey] || "";
334
+ const explanationValue = example[this.explanationKey] || "";
335
+ let userText = "";
336
+ let modelResponse = {};
199
337
 
200
338
  // Add context as user message with special formatting to make it part of the example flow
201
339
  if (contextValue) {
202
- let contextText = u.isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
340
+ let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
203
341
  // Prefix context to make it clear it's contextual information
204
- historyToAdd.push({
205
- role: 'user',
206
- parts: [{ text: `Context: ${contextText}` }]
207
- });
208
- // Add a brief model acknowledgment
209
- historyToAdd.push({
210
- role: 'model',
211
- parts: [{ text: "I understand the context." }]
212
- });
342
+ userText += `CONTEXT:\n${contextText}\n\n`;
213
343
  }
214
344
 
215
345
  if (promptValue) {
216
- let promptText = u.isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
217
- historyToAdd.push({ role: 'user', parts: [{ text: promptText }] });
346
+ let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
347
+ userText += promptText;
218
348
  }
219
349
 
220
- if (answerValue) {
221
- let answerText = u.isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : answerValue;
222
- historyToAdd.push({ role: 'model', parts: [{ text: answerText }] });
350
+ if (answerValue) modelResponse.data = answerValue;
351
+ if (explanationValue) modelResponse.explanation = explanationValue;
352
+ const modelText = JSON.stringify(modelResponse, null, 2);
353
+
354
+ if (userText.trim().length && modelText.trim().length > 0) {
355
+ historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
356
+ historyToAdd.push({ role: 'model', parts: [{ text: modelText.trim() }] });
223
357
  }
358
+
224
359
  }
225
360
 
226
- const currentHistory = this?.chat?.getHistory() || [];
227
361
 
362
+ const currentHistory = this?.chat?.getHistory() || [];
363
+ log.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
228
364
  this.chat = await this.genAIClient.chats.create({
229
365
  model: this.modelName,
230
366
  // @ts-ignore
@@ -232,7 +368,10 @@ async function seedWithExamples(examples) {
232
368
  history: [...currentHistory, ...historyToAdd],
233
369
  });
234
370
 
235
- log.debug("Transformation examples seeded successfully.");
371
+
372
+ const newHistory = this.chat.getHistory();
373
+ log.debug(`Created new chat session with ${newHistory.length} examples.`);
374
+ return newHistory;
236
375
  }
237
376
 
238
377
  /**
@@ -241,90 +380,157 @@ async function seedWithExamples(examples) {
241
380
  * @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
242
381
  * @throws {Error} If the transformation fails or returns invalid JSON.
243
382
  */
244
- async function transformJSON(sourcePayload) {
383
+ /**
384
+ * (Internal) Sends a single prompt to the model and parses the response.
385
+ * No validation or retry logic.
386
+ * @this {ExportedAPI}
387
+ * @param {Object|string} sourcePayload - The source payload.
388
+ * @returns {Promise<Object>} - The transformed payload.
389
+ */
390
+ async function rawMessage(sourcePayload) {
245
391
  if (!this.chat) {
246
- throw new Error("Chat session not initialized. Call initChat() or seedWithExamples() first.");
392
+ throw new Error("Chat session not initialized.");
247
393
  }
248
394
 
249
- let result;
250
- let actualPayload;
251
- if (sourcePayload && u.isJSON(sourcePayload)) actualPayload = JSON.stringify(sourcePayload, null, 2);
252
- else if (typeof sourcePayload === 'string') actualPayload = sourcePayload;
253
- else throw new Error("Invalid source payload. Must be a JSON object or a valid JSON string.");
395
+ const actualPayload = typeof sourcePayload === 'string'
396
+ ? sourcePayload
397
+ : JSON.stringify(sourcePayload, null, 2);
254
398
 
255
399
  try {
256
- result = await this.chat.sendMessage({ message: actualPayload });
400
+ const result = await this.chat.sendMessage({ message: actualPayload });
401
+ const modelResponse = result.text;
402
+ const extractedJSON = extractJSON(modelResponse); // Assuming extractJSON is defined
403
+
404
+ // Unwrap the 'data' property if it exists
405
+ if (extractedJSON?.data) {
406
+ return extractedJSON.data;
407
+ }
408
+ return extractedJSON;
409
+
257
410
  } catch (error) {
258
- log.error("Error with Gemini API:", error);
411
+ if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
412
+ throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
413
+ }
414
+ // For other API errors, just re-throw
259
415
  throw new Error(`Transformation failed: ${error.message}`);
260
416
  }
261
-
262
- try {
263
- const modelResponse = result.text;
264
- const parsedResponse = JSON.parse(modelResponse);
265
- return parsedResponse;
266
- } catch (parseError) {
267
- log.error("Error parsing Gemini response:", parseError);
268
- throw new Error(`Invalid JSON response from Gemini: ${parseError.message}`);
269
- }
270
417
  }
271
418
 
272
419
  /**
273
- * Transforms payload with automatic validation and retry logic
274
- * @param {Object} sourcePayload - The source payload to transform
275
- * @param {AsyncValidatorFunction} validatorFn - Async function that validates the transformed payload
276
- * @param {Object} [options] - Options for the validation process
277
- * @param {number} [options.maxRetries] - Override default max retries
278
- * @param {number} [options.retryDelay] - Override default retry delay
279
- * @returns {Promise<Object>} - The validated transformed payload
280
- * @throws {Error} If transformation or validation fails after all retries
420
+ * (Engine) Transforms a payload with validation and automatic retry logic.
421
+ * @this {ExportedAPI}
422
+ * @param {Object} sourcePayload - The source payload to transform.
423
+ * @param {Object} [options] - Options for the validation process.
424
+ * @param {AsyncValidatorFunction | null} validatorFn - The specific validator to use for this run.
425
+ * @returns {Promise<Object>} - The validated transformed payload.
281
426
  */
282
- async function transformWithValidation(sourcePayload, validatorFn, options = {}) {
427
+ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
428
+ if (!this.chat) {
429
+ throw new Error("Chat session not initialized. Please call init() first.");
430
+ }
283
431
  const maxRetries = options.maxRetries ?? this.maxRetries;
284
432
  const retryDelay = options.retryDelay ?? this.retryDelay;
285
433
 
286
- let lastPayload = null;
287
434
  let lastError = null;
435
+ let lastPayload = null; // Store the payload that caused the validation error
436
+
437
+ // Prepare the payload
438
+ if (sourcePayload && isJSON(sourcePayload)) {
439
+ lastPayload = JSON.stringify(sourcePayload, null, 2);
440
+ } else if (typeof sourcePayload === 'string') {
441
+ lastPayload = sourcePayload;
442
+ }
443
+ else if (typeof sourcePayload === 'boolean' || typeof sourcePayload === 'number') {
444
+ lastPayload = sourcePayload.toString();
445
+ }
446
+ else if (sourcePayload === null || sourcePayload === undefined) {
447
+ lastPayload = JSON.stringify({}); // Convert null/undefined to empty object
448
+ }
449
+ else {
450
+ throw new Error("Invalid source payload. Must be a JSON object or string.");
451
+ }
288
452
 
289
453
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
290
454
  try {
291
- // First attempt uses normal transformation, subsequent attempts use rebuild
292
- const transformedPayload = attempt === 0
293
- ? await this.message(sourcePayload)
455
+ // Step 1: Get the transformed payload
456
+ const transformedPayload = (attempt === 0)
457
+ ? await this.rawMessage(lastPayload) // Use the new raw method
294
458
  : await this.rebuild(lastPayload, lastError.message);
295
459
 
296
- // Validate the transformed payload
297
- const validatedPayload = await validatorFn(transformedPayload);
460
+ lastPayload = transformedPayload; // Always update lastPayload *before* validation
298
461
 
299
- log.debug(`Transformation and validation succeeded on attempt ${attempt + 1}`);
300
- return validatedPayload;
462
+ // Step 2: Validate if a validator is provided
463
+ if (validatorFn) {
464
+ await validatorFn(transformedPayload); // Validator throws on failure
465
+ }
466
+
467
+ // Step 3: Success!
468
+ log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
469
+ return transformedPayload;
301
470
 
302
471
  } catch (error) {
303
472
  lastError = error;
473
+ log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
304
474
 
305
- if (attempt === 0) {
306
- // First attempt failed - could be transformation or validation error
307
- lastPayload = await this.message(sourcePayload).catch(() => null);
475
+ if (attempt >= maxRetries) {
476
+ log.error(`All ${maxRetries + 1} attempts failed.`);
477
+ throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
308
478
  }
309
479
 
310
- if (attempt < maxRetries) {
311
- const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
312
- log.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error.message);
313
- await new Promise(res => setTimeout(res, delay));
314
- } else {
315
- log.error(`All ${maxRetries + 1} attempts failed`);
316
- throw new Error(`Transformation with validation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
317
- }
480
+ // Wait before retrying
481
+ const delay = retryDelay * Math.pow(2, attempt);
482
+ await new Promise(res => setTimeout(res, delay));
318
483
  }
319
484
  }
320
485
  }
321
486
 
487
+ /**
488
+ * Rebuilds a payload based on server error feedback
489
+ * @param {Object} lastPayload - The payload that failed validation
490
+ * @param {string} serverError - The error message from the server
491
+ * @returns {Promise<Object>} - A new corrected payload
492
+ * @throws {Error} If the rebuild process fails.
493
+ */
494
+ async function rebuildPayload(lastPayload, serverError) {
495
+ await this.init(); // Ensure chat is initialized
496
+ const prompt = `
497
+ The previous JSON payload (below) failed validation.
498
+ The server's error message is quoted afterward.
499
+
500
+ ---------------- BAD PAYLOAD ----------------
501
+ ${JSON.stringify(lastPayload, null, 2)}
502
+
503
+
504
+ ---------------- SERVER ERROR ----------------
505
+ ${serverError}
506
+
507
+ Please return a NEW JSON payload that corrects the issue.
508
+ Respond with JSON only – no comments or explanations.
509
+ `;
510
+
511
+ let result;
512
+ try {
513
+ result = await this.chat.sendMessage({ message: prompt });
514
+ } catch (err) {
515
+ throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
516
+ }
517
+
518
+ try {
519
+ const text = result.text ?? result.response ?? '';
520
+ return typeof text === 'object' ? text : JSON.parse(text);
521
+ } catch (parseErr) {
522
+ throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
523
+ }
524
+ }
525
+
526
+
527
+
322
528
 
323
529
  /**
324
530
  * Estimate total token usage if you were to send a new payload as the next message.
325
531
  * Considers system instructions, current chat history (including examples), and the new message.
326
532
  * @param {object|string} nextPayload - The next user message to be sent (object or string)
327
- * @returns {Promise<{ totalTokens: number, ... }>} - The result of Gemini's countTokens API
533
+ * @returns {Promise<{ totalTokens: number }>} - The result of Gemini's countTokens API
328
534
  */
329
535
  async function estimateTokenUsage(nextPayload) {
330
536
  // Compose the conversation contents, Gemini-style
@@ -360,44 +566,6 @@ async function estimateTokenUsage(nextPayload) {
360
566
  return resp; // includes totalTokens, possibly breakdown
361
567
  }
362
568
 
363
- /**
364
- * Rebuilds a payload based on server error feedback
365
- * @param {Object} lastPayload - The payload that failed validation
366
- * @param {string} serverError - The error message from the server
367
- * @returns {Promise<Object>} - A new corrected payload
368
- * @throws {Error} If the rebuild process fails.
369
- */
370
- async function rebuildPayload(lastPayload, serverError) {
371
- await this.init();
372
-
373
- const prompt = `
374
- The previous JSON payload (below) failed validation.
375
- The server's error message is quoted afterward.
376
-
377
- ---------------- BAD PAYLOAD ----------------
378
- ${JSON.stringify(lastPayload, null, 2)}
379
-
380
- ---------------- SERVER ERROR ----------------
381
- ${serverError}
382
-
383
- Please return a NEW JSON payload that corrects the issue.
384
- Respond with JSON only – no comments or explanations.
385
- `;
386
-
387
- let result;
388
- try {
389
- result = await this.chat.sendMessage({ message: prompt });
390
- } catch (err) {
391
- throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
392
- }
393
-
394
- try {
395
- const text = result.text ?? result.response ?? '';
396
- return typeof text === 'object' ? text : JSON.parse(text);
397
- } catch (parseErr) {
398
- throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
399
- }
400
- }
401
569
 
402
570
  /**
403
571
  * Resets the current chat session, clearing all history and examples
@@ -432,6 +600,170 @@ function getChatHistory() {
432
600
  }
433
601
 
434
602
 
603
+ /*
604
+ ----
605
+ HELPERS
606
+ ----
607
+ */
608
+
609
+ function isJSON(data) {
610
+ try {
611
+ const attempt = JSON.stringify(data);
612
+ if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
613
+ if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
614
+ return true;
615
+ }
616
+ }
617
+ return false;
618
+ } catch (e) {
619
+ return false;
620
+ }
621
+ }
622
+
623
+ function isJSONStr(string) {
624
+ if (typeof string !== 'string') return false;
625
+ try {
626
+ const result = JSON.parse(string);
627
+ const type = Object.prototype.toString.call(result);
628
+ return type === '[object Object]' || type === '[object Array]';
629
+ } catch (err) {
630
+ return false;
631
+ }
632
+ }
633
+
634
+ function extractJSON(text) {
635
+ if (!text || typeof text !== 'string') {
636
+ throw new Error('No text provided for JSON extraction');
637
+ }
638
+
639
+ // Strategy 1: Try parsing the entire response as JSON
640
+ if (isJSONStr(text.trim())) {
641
+ return JSON.parse(text.trim());
642
+ }
643
+
644
+ // Strategy 2: Look for JSON code blocks (```json...``` or ```...```)
645
+ const codeBlockPatterns = [
646
+ /```json\s*\n?([\s\S]*?)\n?\s*```/gi,
647
+ /```\s*\n?([\s\S]*?)\n?\s*```/gi
648
+ ];
649
+
650
+ for (const pattern of codeBlockPatterns) {
651
+ const matches = text.match(pattern);
652
+ if (matches) {
653
+ for (const match of matches) {
654
+ const jsonContent = match.replace(/```json\s*\n?/gi, '').replace(/```\s*\n?/gi, '').trim();
655
+ if (isJSONStr(jsonContent)) {
656
+ return JSON.parse(jsonContent);
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ // Strategy 3: Look for JSON objects/arrays using bracket matching
663
+ const jsonPatterns = [
664
+ // Match complete JSON objects
665
+ /\{[\s\S]*\}/g,
666
+ // Match complete JSON arrays
667
+ /\[[\s\S]*\]/g
668
+ ];
669
+
670
+ for (const pattern of jsonPatterns) {
671
+ const matches = text.match(pattern);
672
+ if (matches) {
673
+ for (const match of matches) {
674
+ const candidate = match.trim();
675
+ if (isJSONStr(candidate)) {
676
+ return JSON.parse(candidate);
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ // Strategy 4: Advanced bracket matching for nested structures
683
+ const advancedExtract = findCompleteJSONStructures(text);
684
+ if (advancedExtract.length > 0) {
685
+ // Return the first valid JSON structure found
686
+ for (const candidate of advancedExtract) {
687
+ if (isJSONStr(candidate)) {
688
+ return JSON.parse(candidate);
689
+ }
690
+ }
691
+ }
692
+
693
+ // Strategy 5: Clean up common formatting issues and retry
694
+ const cleanedText = text
695
+ .replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, '') // Remove conversational intros
696
+ .replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, '')
697
+ .replace(/^\s*The\s+.*?is\s*[:\n]/gi, '')
698
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* comments */
699
+ .replace(/\/\/.*$/gm, '') // Remove // comments
700
+ .trim();
701
+
702
+ if (isJSONStr(cleanedText)) {
703
+ return JSON.parse(cleanedText);
704
+ }
705
+
706
+ // If all else fails, throw an error with helpful information
707
+ throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
708
+ }
709
+
710
+ function findCompleteJSONStructures(text) {
711
+ const results = [];
712
+ const startChars = ['{', '['];
713
+
714
+ for (let i = 0; i < text.length; i++) {
715
+ if (startChars.includes(text[i])) {
716
+ const extracted = extractCompleteStructure(text, i);
717
+ if (extracted) {
718
+ results.push(extracted);
719
+ }
720
+ }
721
+ }
722
+
723
+ return results;
724
+ }
725
+
726
+
727
+ function extractCompleteStructure(text, startPos) {
728
+ const startChar = text[startPos];
729
+ const endChar = startChar === '{' ? '}' : ']';
730
+ let depth = 0;
731
+ let inString = false;
732
+ let escaped = false;
733
+
734
+ for (let i = startPos; i < text.length; i++) {
735
+ const char = text[i];
736
+
737
+ if (escaped) {
738
+ escaped = false;
739
+ continue;
740
+ }
741
+
742
+ if (char === '\\' && inString) {
743
+ escaped = true;
744
+ continue;
745
+ }
746
+
747
+ if (char === '"' && !escaped) {
748
+ inString = !inString;
749
+ continue;
750
+ }
751
+
752
+ if (!inString) {
753
+ if (char === startChar) {
754
+ depth++;
755
+ } else if (char === endChar) {
756
+ depth--;
757
+ if (depth === 0) {
758
+ return text.substring(startPos, i + 1);
759
+ }
760
+ }
761
+ }
762
+ }
763
+
764
+ return null; // Incomplete structure
765
+ }
766
+
435
767
  if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
436
768
  log.info("RUNNING AI Transformer as standalone script...");
437
769
  (
@@ -439,7 +771,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
439
771
  try {
440
772
  log.info("Initializing AI Transformer...");
441
773
  const transformer = new AITransformer({
442
- modelName: 'gemini-2.0-flash',
774
+ modelName: 'gemini-2.5-flash',
443
775
  sourceKey: 'INPUT', // Custom source key
444
776
  targetKey: 'OUTPUT', // Custom target key
445
777
  contextKey: 'CONTEXT', // Custom context key
@@ -483,7 +815,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
483
815
  return payload; // Return the payload if validation passes
484
816
  };
485
817
 
486
- const validatedResponse = await transformer.transformWithValidation(
818
+ const validatedResponse = await transformer.messageAndValidate(
487
819
  { "name": "Lynn" },
488
820
  mockValidator
489
821
  );