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