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.
- package/README.md +43 -22
- package/index.cjs +288 -69
- package/index.js +424 -89
- 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,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
|
-
|
|
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);
|
|
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.
|
|
130
|
+
this.modelName = options.modelName || 'gemini-2.5-flash';
|
|
110
131
|
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
111
132
|
|
|
112
|
-
|
|
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'; //
|
|
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.
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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 =
|
|
300
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
201
301
|
// Prefix context to make it clear it's contextual information
|
|
202
|
-
|
|
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 =
|
|
215
|
-
|
|
306
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
307
|
+
userText += promptText;
|
|
216
308
|
}
|
|
217
309
|
|
|
218
|
-
if (answerValue)
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
352
|
+
throw new Error("Chat session not initialized.");
|
|
245
353
|
}
|
|
246
354
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
272
|
-
* @
|
|
273
|
-
* @param {
|
|
274
|
-
* @param {Object} [options] - Options for the validation process
|
|
275
|
-
* @param {
|
|
276
|
-
* @
|
|
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
|
|
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
|
-
//
|
|
290
|
-
const transformedPayload = attempt === 0
|
|
291
|
-
? await this.
|
|
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
|
-
//
|
|
295
|
-
const validatedPayload = await validatorFn(transformedPayload);
|
|
420
|
+
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
296
421
|
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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.
|
|
778
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
444
779
|
{ "name": "Lynn" },
|
|
445
780
|
mockValidator
|
|
446
781
|
);
|