ak-gemini 1.0.5 → 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 +4 -3
  2. package/index.cjs +284 -86
  3. package/index.js +417 -125
  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,25 +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);
99
116
  this.estimate = estimateTokenUsage.bind(this);
117
+ this.estimateTokenUsage = estimateTokenUsage.bind(this);
100
118
  }
101
119
  }
102
120
 
121
+ export default AITransformer;
122
+
103
123
  /**
104
124
  * factory function to create an AI Transformer instance
105
125
  * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
@@ -107,10 +127,39 @@ export default class AITransformer {
107
127
  */
108
128
  function AITransformFactory(options = {}) {
109
129
  // ? https://ai.google.dev/gemini-api/docs/models
110
- this.modelName = options.modelName || 'gemini-2.0-flash';
130
+ this.modelName = options.modelName || 'gemini-2.5-flash';
111
131
  this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
112
132
 
113
- 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;
114
163
  if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
115
164
  // Build chat config, making sure systemInstruction uses the custom instructions
116
165
  this.chatConfig = {
@@ -129,20 +178,30 @@ function AITransformFactory(options = {}) {
129
178
  this.exampleData = options.exampleData || null; // can be used instead of examplesFile
130
179
 
131
180
  // 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
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
135
186
 
136
187
  // Retry configuration
137
188
  this.maxRetries = options.maxRetries || 3;
138
189
  this.retryDelay = options.retryDelay || 1000;
139
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
+
140
197
  if (this.promptKey === this.answerKey) {
141
198
  throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
142
199
  }
143
200
 
144
- log.debug(`Creating AI Transformer with model: ${this.modelName}`);
145
- 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
+ }
146
205
 
147
206
  const ai = new GoogleGenAI({ apiKey: this.apiKey });
148
207
  this.genAIClient = ai;
@@ -151,11 +210,12 @@ function AITransformFactory(options = {}) {
151
210
 
152
211
  /**
153
212
  * Initializes the chat session with the specified model and configurations.
213
+ * @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
154
214
  * @this {ExportedAPI}
155
215
  * @returns {Promise<void>}
156
216
  */
157
- async function initChat() {
158
- if (this.chat) return;
217
+ async function initChat(force = false) {
218
+ if (this.chat && !force) return;
159
219
 
160
220
  log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
161
221
 
@@ -166,6 +226,15 @@ async function initChat() {
166
226
  history: [],
167
227
  });
168
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
+
169
238
  log.debug("Gemini chat session initialized.");
170
239
  }
171
240
 
@@ -173,6 +242,7 @@ async function initChat() {
173
242
  * Seeds the chat session with example transformations.
174
243
  * @this {ExportedAPI}
175
244
  * @param {TransformationExample[]} [examples] - An array of transformation examples.
245
+ * @this {ExportedAPI}
176
246
  * @returns {Promise<void>}
177
247
  */
178
248
  async function seedWithExamples(examples) {
@@ -181,13 +251,38 @@ async function seedWithExamples(examples) {
181
251
  if (!examples || !Array.isArray(examples) || examples.length === 0) {
182
252
  if (this.examplesFile) {
183
253
  log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
184
- examples = await u.load(path.resolve(this.examplesFile), true);
185
- } 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 {
186
273
  log.debug("No examples provided and no examples file specified. Skipping seeding.");
187
274
  return;
188
275
  }
189
276
  }
190
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
+
191
286
  log.debug(`Seeding chat with ${examples.length} transformation examples...`);
192
287
  const historyToAdd = [];
193
288
 
@@ -196,35 +291,36 @@ async function seedWithExamples(examples) {
196
291
  const contextValue = example[this.contextKey] || "";
197
292
  const promptValue = example[this.promptKey] || "";
198
293
  const answerValue = example[this.answerKey] || "";
294
+ const explanationValue = example[this.explanationKey] || "";
295
+ let userText = "";
296
+ let modelResponse = {};
199
297
 
200
298
  // Add context as user message with special formatting to make it part of the example flow
201
299
  if (contextValue) {
202
- let contextText = u.isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
300
+ let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
203
301
  // 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
- });
302
+ userText += `CONTEXT:\n${contextText}\n\n`;
213
303
  }
214
304
 
215
305
  if (promptValue) {
216
- let promptText = u.isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
217
- historyToAdd.push({ role: 'user', parts: [{ text: promptText }] });
306
+ let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
307
+ userText += promptText;
218
308
  }
219
309
 
220
- if (answerValue) {
221
- let answerText = u.isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : answerValue;
222
- 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() }] });
223
317
  }
318
+
224
319
  }
225
320
 
226
- const currentHistory = this?.chat?.getHistory() || [];
227
321
 
322
+ const currentHistory = this?.chat?.getHistory() || [];
323
+ log.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
228
324
  this.chat = await this.genAIClient.chats.create({
229
325
  model: this.modelName,
230
326
  // @ts-ignore
@@ -232,7 +328,10 @@ async function seedWithExamples(examples) {
232
328
  history: [...currentHistory, ...historyToAdd],
233
329
  });
234
330
 
235
- 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;
236
335
  }
237
336
 
238
337
  /**
@@ -241,90 +340,157 @@ async function seedWithExamples(examples) {
241
340
  * @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
242
341
  * @throws {Error} If the transformation fails or returns invalid JSON.
243
342
  */
244
- 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) {
245
351
  if (!this.chat) {
246
- throw new Error("Chat session not initialized. Call initChat() or seedWithExamples() first.");
352
+ throw new Error("Chat session not initialized.");
247
353
  }
248
354
 
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.");
355
+ const actualPayload = typeof sourcePayload === 'string'
356
+ ? sourcePayload
357
+ : JSON.stringify(sourcePayload, null, 2);
254
358
 
255
359
  try {
256
- 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
+
257
370
  } catch (error) {
258
- 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
259
375
  throw new Error(`Transformation failed: ${error.message}`);
260
376
  }
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
377
  }
271
378
 
272
379
  /**
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
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.
281
386
  */
282
- 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
+ }
283
391
  const maxRetries = options.maxRetries ?? this.maxRetries;
284
392
  const retryDelay = options.retryDelay ?? this.retryDelay;
285
393
 
286
- let lastPayload = null;
287
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
+ }
288
412
 
289
413
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
290
414
  try {
291
- // First attempt uses normal transformation, subsequent attempts use rebuild
292
- const transformedPayload = attempt === 0
293
- ? 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
294
418
  : await this.rebuild(lastPayload, lastError.message);
295
419
 
296
- // Validate the transformed payload
297
- const validatedPayload = await validatorFn(transformedPayload);
420
+ lastPayload = transformedPayload; // Always update lastPayload *before* validation
298
421
 
299
- log.debug(`Transformation and validation succeeded on attempt ${attempt + 1}`);
300
- 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;
301
430
 
302
431
  } catch (error) {
303
432
  lastError = error;
433
+ log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
304
434
 
305
- if (attempt === 0) {
306
- // First attempt failed - could be transformation or validation error
307
- 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}`);
308
438
  }
309
439
 
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
- }
440
+ // Wait before retrying
441
+ const delay = retryDelay * Math.pow(2, attempt);
442
+ await new Promise(res => setTimeout(res, delay));
318
443
  }
319
444
  }
320
445
  }
321
446
 
447
+ /**
448
+ * Rebuilds a payload based on server error feedback
449
+ * @param {Object} lastPayload - The payload that failed validation
450
+ * @param {string} serverError - The error message from the server
451
+ * @returns {Promise<Object>} - A new corrected payload
452
+ * @throws {Error} If the rebuild process fails.
453
+ */
454
+ async function rebuildPayload(lastPayload, serverError) {
455
+ await this.init(); // Ensure chat is initialized
456
+ const prompt = `
457
+ The previous JSON payload (below) failed validation.
458
+ The server's error message is quoted afterward.
459
+
460
+ ---------------- BAD PAYLOAD ----------------
461
+ ${JSON.stringify(lastPayload, null, 2)}
462
+
463
+
464
+ ---------------- SERVER ERROR ----------------
465
+ ${serverError}
466
+
467
+ Please return a NEW JSON payload that corrects the issue.
468
+ Respond with JSON only – no comments or explanations.
469
+ `;
470
+
471
+ let result;
472
+ try {
473
+ result = await this.chat.sendMessage({ message: prompt });
474
+ } catch (err) {
475
+ throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
476
+ }
477
+
478
+ try {
479
+ const text = result.text ?? result.response ?? '';
480
+ return typeof text === 'object' ? text : JSON.parse(text);
481
+ } catch (parseErr) {
482
+ throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
483
+ }
484
+ }
485
+
486
+
487
+
322
488
 
323
489
  /**
324
490
  * Estimate total token usage if you were to send a new payload as the next message.
325
491
  * Considers system instructions, current chat history (including examples), and the new message.
326
492
  * @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
493
+ * @returns {Promise<{ totalTokens: number }>} - The result of Gemini's countTokens API
328
494
  */
329
495
  async function estimateTokenUsage(nextPayload) {
330
496
  // Compose the conversation contents, Gemini-style
@@ -360,44 +526,6 @@ async function estimateTokenUsage(nextPayload) {
360
526
  return resp; // includes totalTokens, possibly breakdown
361
527
  }
362
528
 
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
529
 
402
530
  /**
403
531
  * Resets the current chat session, clearing all history and examples
@@ -432,6 +560,170 @@ function getChatHistory() {
432
560
  }
433
561
 
434
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
+
435
727
  if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
436
728
  log.info("RUNNING AI Transformer as standalone script...");
437
729
  (
@@ -439,7 +731,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
439
731
  try {
440
732
  log.info("Initializing AI Transformer...");
441
733
  const transformer = new AITransformer({
442
- modelName: 'gemini-2.0-flash',
734
+ modelName: 'gemini-2.5-flash',
443
735
  sourceKey: 'INPUT', // Custom source key
444
736
  targetKey: 'OUTPUT', // Custom target key
445
737
  contextKey: 'CONTEXT', // Custom context key
@@ -483,7 +775,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
483
775
  return payload; // Return the payload if validation passes
484
776
  };
485
777
 
486
- const validatedResponse = await transformer.transformWithValidation(
778
+ const validatedResponse = await transformer.messageAndValidate(
487
779
  { "name": "Lynn" },
488
780
  mockValidator
489
781
  );