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.
- package/README.md +4 -3
- package/index.cjs +284 -86
- package/index.js +417 -125
- package/package.json +21 -16
- package/types.d.ts +125 -0
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
130
|
+
this.modelName = options.modelName || 'gemini-2.5-flash';
|
|
111
131
|
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
112
132
|
|
|
113
|
-
|
|
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'; //
|
|
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.
|
|
145
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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 =
|
|
300
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
203
301
|
// Prefix context to make it clear it's contextual information
|
|
204
|
-
|
|
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 =
|
|
217
|
-
|
|
306
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
307
|
+
userText += promptText;
|
|
218
308
|
}
|
|
219
309
|
|
|
220
|
-
if (answerValue)
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
352
|
+
throw new Error("Chat session not initialized.");
|
|
247
353
|
}
|
|
248
354
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
274
|
-
* @
|
|
275
|
-
* @param {
|
|
276
|
-
* @param {Object} [options] - Options for the validation process
|
|
277
|
-
* @param {
|
|
278
|
-
* @
|
|
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
|
|
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
|
-
//
|
|
292
|
-
const transformedPayload = attempt === 0
|
|
293
|
-
? await this.
|
|
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
|
-
//
|
|
297
|
-
const validatedPayload = await validatorFn(transformedPayload);
|
|
420
|
+
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
298
421
|
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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.
|
|
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.
|
|
778
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
487
779
|
{ "name": "Lynn" },
|
|
488
780
|
mockValidator
|
|
489
781
|
);
|