ak-gemini 1.0.4 → 1.0.6

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