ak-gemini 1.0.51 → 1.0.53
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/index.cjs +230 -79
- package/index.js +345 -114
- package/package.json +3 -3
- package/types.ts +0 -68
package/index.cjs
CHANGED
|
@@ -29,8 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
// index.js
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
-
|
|
33
|
-
default: () => AITransformer,
|
|
32
|
+
default: () => index_default,
|
|
34
33
|
log: () => logger_default
|
|
35
34
|
});
|
|
36
35
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -76,7 +75,7 @@ When presented with new Source JSON, apply the learned transformation rules to p
|
|
|
76
75
|
|
|
77
76
|
Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
|
|
78
77
|
|
|
79
|
-
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
78
|
+
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
80
79
|
`;
|
|
81
80
|
var DEFAULT_CHAT_CONFIG = {
|
|
82
81
|
responseMimeType: "application/json",
|
|
@@ -96,26 +95,34 @@ var AITransformer = class {
|
|
|
96
95
|
this.promptKey = "";
|
|
97
96
|
this.answerKey = "";
|
|
98
97
|
this.contextKey = "";
|
|
98
|
+
this.explanationKey = "";
|
|
99
|
+
this.systemInstructionKey = "";
|
|
99
100
|
this.maxRetries = 3;
|
|
100
101
|
this.retryDelay = 1e3;
|
|
101
102
|
this.systemInstructions = "";
|
|
102
103
|
this.chatConfig = {};
|
|
103
104
|
this.apiKey = GEMINI_API_KEY;
|
|
105
|
+
this.onlyJSON = true;
|
|
106
|
+
this.asyncValidator = null;
|
|
104
107
|
AITransformFactory.call(this, options);
|
|
105
108
|
this.init = initChat.bind(this);
|
|
106
109
|
this.seed = seedWithExamples.bind(this);
|
|
107
|
-
this.
|
|
110
|
+
this.rawMessage = rawMessage.bind(this);
|
|
111
|
+
this.message = (payload, opts = {}, validatorFn = null) => {
|
|
112
|
+
return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
|
|
113
|
+
};
|
|
108
114
|
this.rebuild = rebuildPayload.bind(this);
|
|
109
115
|
this.reset = resetChat.bind(this);
|
|
110
116
|
this.getHistory = getChatHistory.bind(this);
|
|
111
|
-
this.
|
|
117
|
+
this.messageAndValidate = prepareAndValidateMessage.bind(this);
|
|
112
118
|
this.estimate = estimateTokenUsage.bind(this);
|
|
113
119
|
}
|
|
114
120
|
};
|
|
121
|
+
var index_default = AITransformer;
|
|
115
122
|
function AITransformFactory(options = {}) {
|
|
116
123
|
this.modelName = options.modelName || "gemini-2.0-flash";
|
|
117
124
|
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
118
|
-
this.apiKey = options.apiKey
|
|
125
|
+
this.apiKey = options.apiKey !== void 0 && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
|
|
119
126
|
if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
|
|
120
127
|
this.chatConfig = {
|
|
121
128
|
...DEFAULT_CHAT_CONFIG,
|
|
@@ -127,11 +134,15 @@ function AITransformFactory(options = {}) {
|
|
|
127
134
|
}
|
|
128
135
|
this.examplesFile = options.examplesFile || null;
|
|
129
136
|
this.exampleData = options.exampleData || null;
|
|
130
|
-
this.promptKey = options.
|
|
131
|
-
this.answerKey = options.
|
|
137
|
+
this.promptKey = options.promptKey || "PROMPT";
|
|
138
|
+
this.answerKey = options.answerKey || "ANSWER";
|
|
132
139
|
this.contextKey = options.contextKey || "CONTEXT";
|
|
140
|
+
this.explanationKey = options.explanationKey || "EXPLANATION";
|
|
141
|
+
this.systemInstructionsKey = options.systemInstructionsKey || "SYSTEM";
|
|
133
142
|
this.maxRetries = options.maxRetries || 3;
|
|
134
143
|
this.retryDelay = options.retryDelay || 1e3;
|
|
144
|
+
this.asyncValidator = options.asyncValidator || null;
|
|
145
|
+
this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
|
|
135
146
|
if (this.promptKey === this.answerKey) {
|
|
136
147
|
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
137
148
|
}
|
|
@@ -141,8 +152,8 @@ function AITransformFactory(options = {}) {
|
|
|
141
152
|
this.genAIClient = ai;
|
|
142
153
|
this.chat = null;
|
|
143
154
|
}
|
|
144
|
-
async function initChat() {
|
|
145
|
-
if (this.chat) return;
|
|
155
|
+
async function initChat(force = false) {
|
|
156
|
+
if (this.chat && !force) return;
|
|
146
157
|
logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
147
158
|
this.chat = await this.genAIClient.chats.create({
|
|
148
159
|
model: this.modelName,
|
|
@@ -157,36 +168,48 @@ async function seedWithExamples(examples) {
|
|
|
157
168
|
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
158
169
|
if (this.examplesFile) {
|
|
159
170
|
logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
160
|
-
|
|
171
|
+
try {
|
|
172
|
+
examples = await import_ak_tools.default.load(import_path.default.resolve(this.examplesFile), true);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
|
|
175
|
+
}
|
|
161
176
|
} else {
|
|
162
177
|
logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
163
178
|
return;
|
|
164
179
|
}
|
|
165
180
|
}
|
|
181
|
+
if (examples?.slice().pop()[this.systemInstructionsKey]) {
|
|
182
|
+
logger_default.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
183
|
+
this.systemInstructions = examples.slice().pop()[this.systemInstructionsKey];
|
|
184
|
+
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
185
|
+
await this.init(true);
|
|
186
|
+
}
|
|
166
187
|
logger_default.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
167
188
|
const historyToAdd = [];
|
|
168
189
|
for (const example of examples) {
|
|
169
190
|
const contextValue = example[this.contextKey] || "";
|
|
170
191
|
const promptValue = example[this.promptKey] || "";
|
|
171
192
|
const answerValue = example[this.answerKey] || "";
|
|
193
|
+
const explanationValue = example[this.explanationKey] || "";
|
|
194
|
+
let userText = "";
|
|
195
|
+
let modelResponse = {};
|
|
172
196
|
if (contextValue) {
|
|
173
|
-
let contextText =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
historyToAdd.push({
|
|
179
|
-
role: "model",
|
|
180
|
-
parts: [{ text: "I understand the context." }]
|
|
181
|
-
});
|
|
197
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
198
|
+
userText += `CONTEXT:
|
|
199
|
+
${contextText}
|
|
200
|
+
|
|
201
|
+
`;
|
|
182
202
|
}
|
|
183
203
|
if (promptValue) {
|
|
184
|
-
let promptText =
|
|
185
|
-
|
|
204
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
205
|
+
userText += promptText;
|
|
186
206
|
}
|
|
187
|
-
if (answerValue)
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
208
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
209
|
+
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
210
|
+
if (userText.trim().length && modelText.trim().length > 0) {
|
|
211
|
+
historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
|
|
212
|
+
historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
|
|
190
213
|
}
|
|
191
214
|
}
|
|
192
215
|
const currentHistory = this?.chat?.getHistory() || [];
|
|
@@ -197,77 +220,64 @@ async function seedWithExamples(examples) {
|
|
|
197
220
|
history: [...currentHistory, ...historyToAdd]
|
|
198
221
|
});
|
|
199
222
|
logger_default.debug("Transformation examples seeded successfully.");
|
|
223
|
+
return this.chat.getHistory();
|
|
200
224
|
}
|
|
201
|
-
async function
|
|
225
|
+
async function rawMessage(sourcePayload) {
|
|
202
226
|
if (!this.chat) {
|
|
203
|
-
throw new Error("Chat session not initialized.
|
|
227
|
+
throw new Error("Chat session not initialized.");
|
|
204
228
|
}
|
|
205
|
-
|
|
206
|
-
let actualPayload;
|
|
207
|
-
if (sourcePayload && import_ak_tools.default.isJSON(sourcePayload)) actualPayload = JSON.stringify(sourcePayload, null, 2);
|
|
208
|
-
else if (typeof sourcePayload === "string") actualPayload = sourcePayload;
|
|
209
|
-
else throw new Error("Invalid source payload. Must be a JSON object or a valid JSON string.");
|
|
229
|
+
const actualPayload = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
|
|
210
230
|
try {
|
|
211
|
-
result = await this.chat.sendMessage({ message: actualPayload });
|
|
231
|
+
const result = await this.chat.sendMessage({ message: actualPayload });
|
|
232
|
+
const modelResponse = result.text;
|
|
233
|
+
const extractedJSON = extractJSON(modelResponse);
|
|
234
|
+
if (extractedJSON?.data) {
|
|
235
|
+
return extractedJSON.data;
|
|
236
|
+
}
|
|
237
|
+
return extractedJSON;
|
|
212
238
|
} catch (error) {
|
|
213
|
-
|
|
239
|
+
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
240
|
+
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
241
|
+
}
|
|
214
242
|
throw new Error(`Transformation failed: ${error.message}`);
|
|
215
243
|
}
|
|
216
|
-
try {
|
|
217
|
-
const modelResponse = result.text;
|
|
218
|
-
const parsedResponse = JSON.parse(modelResponse);
|
|
219
|
-
return parsedResponse;
|
|
220
|
-
} catch (parseError) {
|
|
221
|
-
logger_default.error("Error parsing Gemini response:", parseError);
|
|
222
|
-
throw new Error(`Invalid JSON response from Gemini: ${parseError.message}`);
|
|
223
|
-
}
|
|
224
244
|
}
|
|
225
|
-
async function
|
|
245
|
+
async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
246
|
+
if (!this.chat) {
|
|
247
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
248
|
+
}
|
|
226
249
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
227
250
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
228
|
-
let lastPayload = null;
|
|
229
251
|
let lastError = null;
|
|
252
|
+
let lastPayload = null;
|
|
253
|
+
if (sourcePayload && isJSON(sourcePayload)) {
|
|
254
|
+
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
255
|
+
} else if (typeof sourcePayload === "string") {
|
|
256
|
+
lastPayload = sourcePayload;
|
|
257
|
+
} else {
|
|
258
|
+
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
259
|
+
}
|
|
230
260
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
231
261
|
try {
|
|
232
|
-
const transformedPayload = attempt === 0 ? await this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
const transformedPayload = attempt === 0 ? await this.rawMessage(lastPayload) : await this.rebuild(lastPayload, lastError.message);
|
|
263
|
+
lastPayload = transformedPayload;
|
|
264
|
+
if (validatorFn) {
|
|
265
|
+
await validatorFn(transformedPayload);
|
|
266
|
+
}
|
|
267
|
+
logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
268
|
+
return transformedPayload;
|
|
236
269
|
} catch (error) {
|
|
237
270
|
lastError = error;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const delay = retryDelay * Math.pow(2, attempt);
|
|
243
|
-
logger_default.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error.message);
|
|
244
|
-
await new Promise((res) => setTimeout(res, delay));
|
|
245
|
-
} else {
|
|
246
|
-
logger_default.error(`All ${maxRetries + 1} attempts failed`);
|
|
247
|
-
throw new Error(`Transformation with validation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
271
|
+
logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
272
|
+
if (attempt >= maxRetries) {
|
|
273
|
+
logger_default.error(`All ${maxRetries + 1} attempts failed.`);
|
|
274
|
+
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
248
275
|
}
|
|
276
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
277
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
}
|
|
252
|
-
async function estimateTokenUsage(nextPayload) {
|
|
253
|
-
const contents = [];
|
|
254
|
-
if (this.systemInstructions) {
|
|
255
|
-
contents.push({ parts: [{ text: this.systemInstructions }] });
|
|
256
|
-
}
|
|
257
|
-
if (this.chat && typeof this.chat.getHistory === "function") {
|
|
258
|
-
const history = this.chat.getHistory();
|
|
259
|
-
if (Array.isArray(history) && history.length > 0) {
|
|
260
|
-
contents.push(...history);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
|
|
264
|
-
contents.push({ parts: [{ text: nextMessage }] });
|
|
265
|
-
const resp = await this.genAIClient.models.countTokens({
|
|
266
|
-
model: this.modelName,
|
|
267
|
-
contents
|
|
268
|
-
});
|
|
269
|
-
return resp;
|
|
270
|
-
}
|
|
271
281
|
async function rebuildPayload(lastPayload, serverError) {
|
|
272
282
|
await this.init();
|
|
273
283
|
const prompt = `
|
|
@@ -277,6 +287,7 @@ The server's error message is quoted afterward.
|
|
|
277
287
|
---------------- BAD PAYLOAD ----------------
|
|
278
288
|
${JSON.stringify(lastPayload, null, 2)}
|
|
279
289
|
|
|
290
|
+
|
|
280
291
|
---------------- SERVER ERROR ----------------
|
|
281
292
|
${serverError}
|
|
282
293
|
|
|
@@ -296,6 +307,25 @@ Respond with JSON only \u2013 no comments or explanations.
|
|
|
296
307
|
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
297
308
|
}
|
|
298
309
|
}
|
|
310
|
+
async function estimateTokenUsage(nextPayload) {
|
|
311
|
+
const contents = [];
|
|
312
|
+
if (this.systemInstructions) {
|
|
313
|
+
contents.push({ parts: [{ text: this.systemInstructions }] });
|
|
314
|
+
}
|
|
315
|
+
if (this.chat && typeof this.chat.getHistory === "function") {
|
|
316
|
+
const history = this.chat.getHistory();
|
|
317
|
+
if (Array.isArray(history) && history.length > 0) {
|
|
318
|
+
contents.push(...history);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
|
|
322
|
+
contents.push({ parts: [{ text: nextMessage }] });
|
|
323
|
+
const resp = await this.genAIClient.models.countTokens({
|
|
324
|
+
model: this.modelName,
|
|
325
|
+
contents
|
|
326
|
+
});
|
|
327
|
+
return resp;
|
|
328
|
+
}
|
|
299
329
|
async function resetChat() {
|
|
300
330
|
if (this.chat) {
|
|
301
331
|
logger_default.debug("Resetting Gemini chat session...");
|
|
@@ -317,6 +347,128 @@ function getChatHistory() {
|
|
|
317
347
|
}
|
|
318
348
|
return this.chat.getHistory();
|
|
319
349
|
}
|
|
350
|
+
function isJSON(data) {
|
|
351
|
+
try {
|
|
352
|
+
const attempt = JSON.stringify(data);
|
|
353
|
+
if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
|
|
354
|
+
if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function isJSONStr(string) {
|
|
364
|
+
if (typeof string !== "string") return false;
|
|
365
|
+
try {
|
|
366
|
+
const result = JSON.parse(string);
|
|
367
|
+
const type = Object.prototype.toString.call(result);
|
|
368
|
+
return type === "[object Object]" || type === "[object Array]";
|
|
369
|
+
} catch (err) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function extractJSON(text) {
|
|
374
|
+
if (!text || typeof text !== "string") {
|
|
375
|
+
throw new Error("No text provided for JSON extraction");
|
|
376
|
+
}
|
|
377
|
+
if (isJSONStr(text.trim())) {
|
|
378
|
+
return JSON.parse(text.trim());
|
|
379
|
+
}
|
|
380
|
+
const codeBlockPatterns = [
|
|
381
|
+
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
382
|
+
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
383
|
+
];
|
|
384
|
+
for (const pattern of codeBlockPatterns) {
|
|
385
|
+
const matches = text.match(pattern);
|
|
386
|
+
if (matches) {
|
|
387
|
+
for (const match of matches) {
|
|
388
|
+
const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
|
|
389
|
+
if (isJSONStr(jsonContent)) {
|
|
390
|
+
return JSON.parse(jsonContent);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const jsonPatterns = [
|
|
396
|
+
// Match complete JSON objects
|
|
397
|
+
/\{[\s\S]*\}/g,
|
|
398
|
+
// Match complete JSON arrays
|
|
399
|
+
/\[[\s\S]*\]/g
|
|
400
|
+
];
|
|
401
|
+
for (const pattern of jsonPatterns) {
|
|
402
|
+
const matches = text.match(pattern);
|
|
403
|
+
if (matches) {
|
|
404
|
+
for (const match of matches) {
|
|
405
|
+
const candidate = match.trim();
|
|
406
|
+
if (isJSONStr(candidate)) {
|
|
407
|
+
return JSON.parse(candidate);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const advancedExtract = findCompleteJSONStructures(text);
|
|
413
|
+
if (advancedExtract.length > 0) {
|
|
414
|
+
for (const candidate of advancedExtract) {
|
|
415
|
+
if (isJSONStr(candidate)) {
|
|
416
|
+
return JSON.parse(candidate);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const cleanedText = text.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, "").replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, "").replace(/^\s*The\s+.*?is\s*[:\n]/gi, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
|
|
421
|
+
if (isJSONStr(cleanedText)) {
|
|
422
|
+
return JSON.parse(cleanedText);
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
425
|
+
}
|
|
426
|
+
function findCompleteJSONStructures(text) {
|
|
427
|
+
const results = [];
|
|
428
|
+
const startChars = ["{", "["];
|
|
429
|
+
for (let i = 0; i < text.length; i++) {
|
|
430
|
+
if (startChars.includes(text[i])) {
|
|
431
|
+
const extracted = extractCompleteStructure(text, i);
|
|
432
|
+
if (extracted) {
|
|
433
|
+
results.push(extracted);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
}
|
|
439
|
+
function extractCompleteStructure(text, startPos) {
|
|
440
|
+
const startChar = text[startPos];
|
|
441
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
442
|
+
let depth = 0;
|
|
443
|
+
let inString = false;
|
|
444
|
+
let escaped = false;
|
|
445
|
+
for (let i = startPos; i < text.length; i++) {
|
|
446
|
+
const char = text[i];
|
|
447
|
+
if (escaped) {
|
|
448
|
+
escaped = false;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (char === "\\" && inString) {
|
|
452
|
+
escaped = true;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (char === '"' && !escaped) {
|
|
456
|
+
inString = !inString;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!inString) {
|
|
460
|
+
if (char === startChar) {
|
|
461
|
+
depth++;
|
|
462
|
+
} else if (char === endChar) {
|
|
463
|
+
depth--;
|
|
464
|
+
if (depth === 0) {
|
|
465
|
+
return text.substring(startPos, i + 1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
320
472
|
if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
321
473
|
logger_default.info("RUNNING AI Transformer as standalone script...");
|
|
322
474
|
(async () => {
|
|
@@ -361,7 +513,7 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
361
513
|
}
|
|
362
514
|
return payload;
|
|
363
515
|
};
|
|
364
|
-
const validatedResponse = await transformer.
|
|
516
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
365
517
|
{ "name": "Lynn" },
|
|
366
518
|
mockValidator
|
|
367
519
|
);
|
|
@@ -375,6 +527,5 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
375
527
|
}
|
|
376
528
|
// Annotate the CommonJS export names for ESM import in node:
|
|
377
529
|
0 && (module.exports = {
|
|
378
|
-
AITransformer,
|
|
379
530
|
log
|
|
380
531
|
});
|
package/index.js
CHANGED
|
@@ -52,7 +52,7 @@ When presented with new Source JSON, apply the learned transformation rules to p
|
|
|
52
52
|
|
|
53
53
|
Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
|
|
54
54
|
|
|
55
|
-
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
55
|
+
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
56
56
|
`;
|
|
57
57
|
|
|
58
58
|
const DEFAULT_CHAT_CONFIG = {
|
|
@@ -64,14 +64,20 @@ const DEFAULT_CHAT_CONFIG = {
|
|
|
64
64
|
safetySettings: DEFAULT_SAFETY_SETTINGS
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {import('./types').AITransformer} AITransformerUtility
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
/**
|
|
68
74
|
* main export class for AI Transformer
|
|
69
75
|
* @class AITransformer
|
|
76
|
+
* @type {AITransformerUtility}
|
|
70
77
|
* @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
|
|
71
78
|
* @implements {ExportedAPI}
|
|
72
79
|
*/
|
|
73
|
-
|
|
74
|
-
export default class AITransformer {
|
|
80
|
+
class AITransformer {
|
|
75
81
|
/**
|
|
76
82
|
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
77
83
|
*
|
|
@@ -81,25 +87,40 @@ export default class AITransformer {
|
|
|
81
87
|
this.promptKey = "";
|
|
82
88
|
this.answerKey = "";
|
|
83
89
|
this.contextKey = "";
|
|
90
|
+
this.explanationKey = "";
|
|
91
|
+
this.systemInstructionKey = "";
|
|
84
92
|
this.maxRetries = 3;
|
|
85
93
|
this.retryDelay = 1000;
|
|
86
94
|
this.systemInstructions = "";
|
|
87
95
|
this.chatConfig = {};
|
|
88
96
|
this.apiKey = GEMINI_API_KEY;
|
|
97
|
+
this.onlyJSON = true; // always return JSON
|
|
98
|
+
this.asyncValidator = null; // for transformWithValidation
|
|
89
99
|
AITransformFactory.call(this, options);
|
|
90
100
|
|
|
91
101
|
//external API
|
|
92
102
|
this.init = initChat.bind(this);
|
|
93
103
|
this.seed = seedWithExamples.bind(this);
|
|
94
|
-
|
|
104
|
+
|
|
105
|
+
// Internal "raw" message sender
|
|
106
|
+
this.rawMessage = rawMessage.bind(this);
|
|
107
|
+
|
|
108
|
+
// The public `.message()` method uses the GLOBAL validator
|
|
109
|
+
this.message = (payload, opts = {}, validatorFn = null) => {
|
|
110
|
+
|
|
111
|
+
return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
|
|
112
|
+
};
|
|
113
|
+
|
|
95
114
|
this.rebuild = rebuildPayload.bind(this);
|
|
96
115
|
this.reset = resetChat.bind(this);
|
|
97
116
|
this.getHistory = getChatHistory.bind(this);
|
|
98
|
-
this.
|
|
117
|
+
this.messageAndValidate = prepareAndValidateMessage.bind(this);
|
|
99
118
|
this.estimate = estimateTokenUsage.bind(this);
|
|
100
119
|
}
|
|
101
120
|
}
|
|
102
|
-
|
|
121
|
+
|
|
122
|
+
export default AITransformer;
|
|
123
|
+
|
|
103
124
|
/**
|
|
104
125
|
* factory function to create an AI Transformer instance
|
|
105
126
|
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
@@ -110,7 +131,7 @@ function AITransformFactory(options = {}) {
|
|
|
110
131
|
this.modelName = options.modelName || 'gemini-2.0-flash';
|
|
111
132
|
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
112
133
|
|
|
113
|
-
this.apiKey = options.apiKey
|
|
134
|
+
this.apiKey = options.apiKey !== undefined && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
|
|
114
135
|
if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
|
|
115
136
|
// Build chat config, making sure systemInstruction uses the custom instructions
|
|
116
137
|
this.chatConfig = {
|
|
@@ -129,14 +150,22 @@ function AITransformFactory(options = {}) {
|
|
|
129
150
|
this.exampleData = options.exampleData || null; // can be used instead of examplesFile
|
|
130
151
|
|
|
131
152
|
// Use configurable keys with fallbacks
|
|
132
|
-
this.promptKey = options.
|
|
133
|
-
this.answerKey = options.
|
|
134
|
-
this.contextKey = options.contextKey || 'CONTEXT'; //
|
|
153
|
+
this.promptKey = options.promptKey || 'PROMPT';
|
|
154
|
+
this.answerKey = options.answerKey || 'ANSWER';
|
|
155
|
+
this.contextKey = options.contextKey || 'CONTEXT'; // Optional key for context
|
|
156
|
+
this.explanationKey = options.explanationKey || 'EXPLANATION'; // Optional key for explanations
|
|
157
|
+
this.systemInstructionsKey = options.systemInstructionsKey || 'SYSTEM'; // Optional key for system instructions
|
|
135
158
|
|
|
136
159
|
// Retry configuration
|
|
137
160
|
this.maxRetries = options.maxRetries || 3;
|
|
138
161
|
this.retryDelay = options.retryDelay || 1000;
|
|
139
162
|
|
|
163
|
+
//allow async validation function
|
|
164
|
+
this.asyncValidator = options.asyncValidator || null; // Function to validate transformed payloads
|
|
165
|
+
|
|
166
|
+
//are we forcing json responses only?
|
|
167
|
+
this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
|
|
168
|
+
|
|
140
169
|
if (this.promptKey === this.answerKey) {
|
|
141
170
|
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
142
171
|
}
|
|
@@ -151,11 +180,12 @@ function AITransformFactory(options = {}) {
|
|
|
151
180
|
|
|
152
181
|
/**
|
|
153
182
|
* Initializes the chat session with the specified model and configurations.
|
|
183
|
+
* @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
|
|
154
184
|
* @this {ExportedAPI}
|
|
155
185
|
* @returns {Promise<void>}
|
|
156
186
|
*/
|
|
157
|
-
async function initChat() {
|
|
158
|
-
if (this.chat) return;
|
|
187
|
+
async function initChat(force = false) {
|
|
188
|
+
if (this.chat && !force) return;
|
|
159
189
|
|
|
160
190
|
log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
161
191
|
|
|
@@ -173,6 +203,7 @@ async function initChat() {
|
|
|
173
203
|
* Seeds the chat session with example transformations.
|
|
174
204
|
* @this {ExportedAPI}
|
|
175
205
|
* @param {TransformationExample[]} [examples] - An array of transformation examples.
|
|
206
|
+
* @this {ExportedAPI}
|
|
176
207
|
* @returns {Promise<void>}
|
|
177
208
|
*/
|
|
178
209
|
async function seedWithExamples(examples) {
|
|
@@ -181,13 +212,25 @@ async function seedWithExamples(examples) {
|
|
|
181
212
|
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
182
213
|
if (this.examplesFile) {
|
|
183
214
|
log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
184
|
-
|
|
215
|
+
try {
|
|
216
|
+
examples = await u.load(path.resolve(this.examplesFile), true);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
|
|
220
|
+
}
|
|
185
221
|
} else {
|
|
186
222
|
log.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
187
223
|
return;
|
|
188
224
|
}
|
|
189
225
|
}
|
|
190
226
|
|
|
227
|
+
if (examples?.slice().pop()[this.systemInstructionsKey]) {
|
|
228
|
+
log.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
229
|
+
this.systemInstructions = examples.slice().pop()[this.systemInstructionsKey];
|
|
230
|
+
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
231
|
+
await this.init(true); // Reinitialize chat with new system instructions
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
log.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
192
235
|
const historyToAdd = [];
|
|
193
236
|
|
|
@@ -196,31 +239,31 @@ async function seedWithExamples(examples) {
|
|
|
196
239
|
const contextValue = example[this.contextKey] || "";
|
|
197
240
|
const promptValue = example[this.promptKey] || "";
|
|
198
241
|
const answerValue = example[this.answerKey] || "";
|
|
242
|
+
const explanationValue = example[this.explanationKey] || "";
|
|
243
|
+
let userText = "";
|
|
244
|
+
let modelResponse = {};
|
|
199
245
|
|
|
200
246
|
// Add context as user message with special formatting to make it part of the example flow
|
|
201
247
|
if (contextValue) {
|
|
202
|
-
let contextText =
|
|
248
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
203
249
|
// 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
|
-
});
|
|
250
|
+
userText += `CONTEXT:\n${contextText}\n\n`;
|
|
213
251
|
}
|
|
214
252
|
|
|
215
253
|
if (promptValue) {
|
|
216
|
-
let promptText =
|
|
217
|
-
|
|
254
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
255
|
+
userText += promptText;
|
|
218
256
|
}
|
|
219
257
|
|
|
220
|
-
if (answerValue)
|
|
221
|
-
|
|
222
|
-
|
|
258
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
259
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
260
|
+
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
261
|
+
|
|
262
|
+
if (userText.trim().length && modelText.trim().length > 0) {
|
|
263
|
+
historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
|
|
264
|
+
historyToAdd.push({ role: 'model', parts: [{ text: modelText.trim() }] });
|
|
223
265
|
}
|
|
266
|
+
|
|
224
267
|
}
|
|
225
268
|
|
|
226
269
|
const currentHistory = this?.chat?.getHistory() || [];
|
|
@@ -233,6 +276,8 @@ async function seedWithExamples(examples) {
|
|
|
233
276
|
});
|
|
234
277
|
|
|
235
278
|
log.debug("Transformation examples seeded successfully.");
|
|
279
|
+
|
|
280
|
+
return this.chat.getHistory(); // Return the updated chat history for reference
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
/**
|
|
@@ -241,90 +286,150 @@ async function seedWithExamples(examples) {
|
|
|
241
286
|
* @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
|
|
242
287
|
* @throws {Error} If the transformation fails or returns invalid JSON.
|
|
243
288
|
*/
|
|
244
|
-
|
|
289
|
+
/**
|
|
290
|
+
* (Internal) Sends a single prompt to the model and parses the response.
|
|
291
|
+
* No validation or retry logic.
|
|
292
|
+
* @this {ExportedAPI}
|
|
293
|
+
* @param {Object|string} sourcePayload - The source payload.
|
|
294
|
+
* @returns {Promise<Object>} - The transformed payload.
|
|
295
|
+
*/
|
|
296
|
+
async function rawMessage(sourcePayload) {
|
|
245
297
|
if (!this.chat) {
|
|
246
|
-
throw new Error("Chat session not initialized.
|
|
298
|
+
throw new Error("Chat session not initialized.");
|
|
247
299
|
}
|
|
248
300
|
|
|
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.");
|
|
301
|
+
const actualPayload = typeof sourcePayload === 'string'
|
|
302
|
+
? sourcePayload
|
|
303
|
+
: JSON.stringify(sourcePayload, null, 2);
|
|
254
304
|
|
|
255
305
|
try {
|
|
256
|
-
result = await this.chat.sendMessage({ message: actualPayload });
|
|
306
|
+
const result = await this.chat.sendMessage({ message: actualPayload });
|
|
307
|
+
const modelResponse = result.text;
|
|
308
|
+
const extractedJSON = extractJSON(modelResponse); // Assuming extractJSON is defined
|
|
309
|
+
|
|
310
|
+
// Unwrap the 'data' property if it exists
|
|
311
|
+
if (extractedJSON?.data) {
|
|
312
|
+
return extractedJSON.data;
|
|
313
|
+
}
|
|
314
|
+
return extractedJSON;
|
|
315
|
+
|
|
257
316
|
} catch (error) {
|
|
258
|
-
|
|
317
|
+
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
318
|
+
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
319
|
+
}
|
|
320
|
+
// For other API errors, just re-throw
|
|
259
321
|
throw new Error(`Transformation failed: ${error.message}`);
|
|
260
322
|
}
|
|
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
323
|
}
|
|
271
324
|
|
|
272
325
|
/**
|
|
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
|
|
326
|
+
* (Engine) Transforms a payload with validation and automatic retry logic.
|
|
327
|
+
* @this {ExportedAPI}
|
|
328
|
+
* @param {Object} sourcePayload - The source payload to transform.
|
|
329
|
+
* @param {Object} [options] - Options for the validation process.
|
|
330
|
+
* @param {AsyncValidatorFunction | null} validatorFn - The specific validator to use for this run.
|
|
331
|
+
* @returns {Promise<Object>} - The validated transformed payload.
|
|
281
332
|
*/
|
|
282
|
-
async function
|
|
333
|
+
async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
334
|
+
if (!this.chat) {
|
|
335
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
336
|
+
}
|
|
283
337
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
284
338
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
285
339
|
|
|
286
|
-
let lastPayload = null;
|
|
287
340
|
let lastError = null;
|
|
341
|
+
let lastPayload = null; // Store the payload that caused the validation error
|
|
342
|
+
|
|
343
|
+
// Prepare the payload
|
|
344
|
+
if (sourcePayload && isJSON(sourcePayload)) {
|
|
345
|
+
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
346
|
+
} else if (typeof sourcePayload === 'string') {
|
|
347
|
+
lastPayload = sourcePayload;
|
|
348
|
+
} else {
|
|
349
|
+
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
350
|
+
}
|
|
288
351
|
|
|
289
352
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
290
353
|
try {
|
|
291
|
-
//
|
|
292
|
-
const transformedPayload = attempt === 0
|
|
293
|
-
? await this.
|
|
354
|
+
// Step 1: Get the transformed payload
|
|
355
|
+
const transformedPayload = (attempt === 0)
|
|
356
|
+
? await this.rawMessage(lastPayload) // Use the new raw method
|
|
294
357
|
: await this.rebuild(lastPayload, lastError.message);
|
|
295
358
|
|
|
296
|
-
//
|
|
297
|
-
const validatedPayload = await validatorFn(transformedPayload);
|
|
359
|
+
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
298
360
|
|
|
299
|
-
|
|
300
|
-
|
|
361
|
+
// Step 2: Validate if a validator is provided
|
|
362
|
+
if (validatorFn) {
|
|
363
|
+
await validatorFn(transformedPayload); // Validator throws on failure
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Step 3: Success!
|
|
367
|
+
log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
368
|
+
return transformedPayload;
|
|
301
369
|
|
|
302
370
|
} catch (error) {
|
|
303
371
|
lastError = error;
|
|
372
|
+
log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
304
373
|
|
|
305
|
-
if (attempt
|
|
306
|
-
|
|
307
|
-
|
|
374
|
+
if (attempt >= maxRetries) {
|
|
375
|
+
log.error(`All ${maxRetries + 1} attempts failed.`);
|
|
376
|
+
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
308
377
|
}
|
|
309
378
|
|
|
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
|
-
}
|
|
379
|
+
// Wait before retrying
|
|
380
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
381
|
+
await new Promise(res => setTimeout(res, delay));
|
|
318
382
|
}
|
|
319
383
|
}
|
|
320
384
|
}
|
|
321
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Rebuilds a payload based on server error feedback
|
|
388
|
+
* @param {Object} lastPayload - The payload that failed validation
|
|
389
|
+
* @param {string} serverError - The error message from the server
|
|
390
|
+
* @returns {Promise<Object>} - A new corrected payload
|
|
391
|
+
* @throws {Error} If the rebuild process fails.
|
|
392
|
+
*/
|
|
393
|
+
async function rebuildPayload(lastPayload, serverError) {
|
|
394
|
+
await this.init(); // Ensure chat is initialized
|
|
395
|
+
const prompt = `
|
|
396
|
+
The previous JSON payload (below) failed validation.
|
|
397
|
+
The server's error message is quoted afterward.
|
|
398
|
+
|
|
399
|
+
---------------- BAD PAYLOAD ----------------
|
|
400
|
+
${JSON.stringify(lastPayload, null, 2)}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
---------------- SERVER ERROR ----------------
|
|
404
|
+
${serverError}
|
|
405
|
+
|
|
406
|
+
Please return a NEW JSON payload that corrects the issue.
|
|
407
|
+
Respond with JSON only – no comments or explanations.
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
let result;
|
|
411
|
+
try {
|
|
412
|
+
result = await this.chat.sendMessage({ message: prompt });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const text = result.text ?? result.response ?? '';
|
|
419
|
+
return typeof text === 'object' ? text : JSON.parse(text);
|
|
420
|
+
} catch (parseErr) {
|
|
421
|
+
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
322
427
|
|
|
323
428
|
/**
|
|
324
429
|
* Estimate total token usage if you were to send a new payload as the next message.
|
|
325
430
|
* Considers system instructions, current chat history (including examples), and the new message.
|
|
326
431
|
* @param {object|string} nextPayload - The next user message to be sent (object or string)
|
|
327
|
-
* @returns {Promise<{ totalTokens: number
|
|
432
|
+
* @returns {Promise<{ totalTokens: number }>} - The result of Gemini's countTokens API
|
|
328
433
|
*/
|
|
329
434
|
async function estimateTokenUsage(nextPayload) {
|
|
330
435
|
// Compose the conversation contents, Gemini-style
|
|
@@ -360,44 +465,6 @@ async function estimateTokenUsage(nextPayload) {
|
|
|
360
465
|
return resp; // includes totalTokens, possibly breakdown
|
|
361
466
|
}
|
|
362
467
|
|
|
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
468
|
|
|
402
469
|
/**
|
|
403
470
|
* Resets the current chat session, clearing all history and examples
|
|
@@ -432,6 +499,170 @@ function getChatHistory() {
|
|
|
432
499
|
}
|
|
433
500
|
|
|
434
501
|
|
|
502
|
+
/*
|
|
503
|
+
----
|
|
504
|
+
HELPERS
|
|
505
|
+
----
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
function isJSON(data) {
|
|
509
|
+
try {
|
|
510
|
+
const attempt = JSON.stringify(data);
|
|
511
|
+
if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
|
|
512
|
+
if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
} catch (e) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isJSONStr(string) {
|
|
523
|
+
if (typeof string !== 'string') return false;
|
|
524
|
+
try {
|
|
525
|
+
const result = JSON.parse(string);
|
|
526
|
+
const type = Object.prototype.toString.call(result);
|
|
527
|
+
return type === '[object Object]' || type === '[object Array]';
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function extractJSON(text) {
|
|
534
|
+
if (!text || typeof text !== 'string') {
|
|
535
|
+
throw new Error('No text provided for JSON extraction');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Strategy 1: Try parsing the entire response as JSON
|
|
539
|
+
if (isJSONStr(text.trim())) {
|
|
540
|
+
return JSON.parse(text.trim());
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Strategy 2: Look for JSON code blocks (```json...``` or ```...```)
|
|
544
|
+
const codeBlockPatterns = [
|
|
545
|
+
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
546
|
+
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
for (const pattern of codeBlockPatterns) {
|
|
550
|
+
const matches = text.match(pattern);
|
|
551
|
+
if (matches) {
|
|
552
|
+
for (const match of matches) {
|
|
553
|
+
const jsonContent = match.replace(/```json\s*\n?/gi, '').replace(/```\s*\n?/gi, '').trim();
|
|
554
|
+
if (isJSONStr(jsonContent)) {
|
|
555
|
+
return JSON.parse(jsonContent);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Strategy 3: Look for JSON objects/arrays using bracket matching
|
|
562
|
+
const jsonPatterns = [
|
|
563
|
+
// Match complete JSON objects
|
|
564
|
+
/\{[\s\S]*\}/g,
|
|
565
|
+
// Match complete JSON arrays
|
|
566
|
+
/\[[\s\S]*\]/g
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
for (const pattern of jsonPatterns) {
|
|
570
|
+
const matches = text.match(pattern);
|
|
571
|
+
if (matches) {
|
|
572
|
+
for (const match of matches) {
|
|
573
|
+
const candidate = match.trim();
|
|
574
|
+
if (isJSONStr(candidate)) {
|
|
575
|
+
return JSON.parse(candidate);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Strategy 4: Advanced bracket matching for nested structures
|
|
582
|
+
const advancedExtract = findCompleteJSONStructures(text);
|
|
583
|
+
if (advancedExtract.length > 0) {
|
|
584
|
+
// Return the first valid JSON structure found
|
|
585
|
+
for (const candidate of advancedExtract) {
|
|
586
|
+
if (isJSONStr(candidate)) {
|
|
587
|
+
return JSON.parse(candidate);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Strategy 5: Clean up common formatting issues and retry
|
|
593
|
+
const cleanedText = text
|
|
594
|
+
.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, '') // Remove conversational intros
|
|
595
|
+
.replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, '')
|
|
596
|
+
.replace(/^\s*The\s+.*?is\s*[:\n]/gi, '')
|
|
597
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* comments */
|
|
598
|
+
.replace(/\/\/.*$/gm, '') // Remove // comments
|
|
599
|
+
.trim();
|
|
600
|
+
|
|
601
|
+
if (isJSONStr(cleanedText)) {
|
|
602
|
+
return JSON.parse(cleanedText);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// If all else fails, throw an error with helpful information
|
|
606
|
+
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function findCompleteJSONStructures(text) {
|
|
610
|
+
const results = [];
|
|
611
|
+
const startChars = ['{', '['];
|
|
612
|
+
|
|
613
|
+
for (let i = 0; i < text.length; i++) {
|
|
614
|
+
if (startChars.includes(text[i])) {
|
|
615
|
+
const extracted = extractCompleteStructure(text, i);
|
|
616
|
+
if (extracted) {
|
|
617
|
+
results.push(extracted);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return results;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
function extractCompleteStructure(text, startPos) {
|
|
627
|
+
const startChar = text[startPos];
|
|
628
|
+
const endChar = startChar === '{' ? '}' : ']';
|
|
629
|
+
let depth = 0;
|
|
630
|
+
let inString = false;
|
|
631
|
+
let escaped = false;
|
|
632
|
+
|
|
633
|
+
for (let i = startPos; i < text.length; i++) {
|
|
634
|
+
const char = text[i];
|
|
635
|
+
|
|
636
|
+
if (escaped) {
|
|
637
|
+
escaped = false;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (char === '\\' && inString) {
|
|
642
|
+
escaped = true;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (char === '"' && !escaped) {
|
|
647
|
+
inString = !inString;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!inString) {
|
|
652
|
+
if (char === startChar) {
|
|
653
|
+
depth++;
|
|
654
|
+
} else if (char === endChar) {
|
|
655
|
+
depth--;
|
|
656
|
+
if (depth === 0) {
|
|
657
|
+
return text.substring(startPos, i + 1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return null; // Incomplete structure
|
|
664
|
+
}
|
|
665
|
+
|
|
435
666
|
if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
436
667
|
log.info("RUNNING AI Transformer as standalone script...");
|
|
437
668
|
(
|
|
@@ -483,7 +714,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
483
714
|
return payload; // Return the payload if validation passes
|
|
484
715
|
};
|
|
485
716
|
|
|
486
|
-
const validatedResponse = await transformer.
|
|
717
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
487
718
|
{ "name": "Lynn" },
|
|
488
719
|
mockValidator
|
|
489
720
|
);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ak-gemini",
|
|
3
3
|
"author": "ak@mixpanel.com",
|
|
4
4
|
"description": "AK's Generative AI Helper for doing... transforms",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.53",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"types.ts",
|
|
11
11
|
"logger.js"
|
|
12
12
|
],
|
|
13
|
-
"types": "types.ts",
|
|
13
|
+
"types": "types.d.ts",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"import": "./index.js",
|
|
@@ -59,4 +59,4 @@
|
|
|
59
59
|
"jest": "^29.7.0",
|
|
60
60
|
"nodemon": "^3.1.10"
|
|
61
61
|
}
|
|
62
|
-
}
|
|
62
|
+
}
|
package/types.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { GoogleGenAI } from '@google/genai';
|
|
2
|
-
|
|
3
|
-
export interface SafetySetting {
|
|
4
|
-
category: string; // The harm category
|
|
5
|
-
threshold: string; // The blocking threshold
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ChatConfig {
|
|
9
|
-
responseMimeType?: string; // MIME type for responses
|
|
10
|
-
temperature?: number; // Controls randomness (0.0 to 1.0)
|
|
11
|
-
topP?: number; // Controls diversity via nucleus sampling
|
|
12
|
-
topK?: number; // Controls diversity by limiting top-k tokens
|
|
13
|
-
systemInstruction?: string; // System instruction for the model
|
|
14
|
-
safetySettings?: SafetySetting[]; // Safety settings array
|
|
15
|
-
responseSchema?: Object; // Schema for validating model responses
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AITransformerContext {
|
|
19
|
-
modelName?: string;
|
|
20
|
-
systemInstructions?: string;
|
|
21
|
-
chatConfig?: ChatConfig;
|
|
22
|
-
genAI?: any;
|
|
23
|
-
chat?: any;
|
|
24
|
-
examplesFile?: string | null;
|
|
25
|
-
exampleData?: TransformationExample[] | null;
|
|
26
|
-
promptKey?: string;
|
|
27
|
-
answerKey?: string;
|
|
28
|
-
contextKey?: string;
|
|
29
|
-
maxRetries?: number;
|
|
30
|
-
retryDelay?: number;
|
|
31
|
-
init?: () => Promise<void>; // Initialization function
|
|
32
|
-
seed?: () => Promise<void>; // Function to seed the transformer with examples
|
|
33
|
-
message?: (payload: Record<string, unknown>) => Promise<Record<string, unknown>>; // Function to send messages to the model
|
|
34
|
-
genAIClient?: GoogleGenAI; // Google GenAI client instance
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface TransformationExample {
|
|
39
|
-
CONTEXT?: Record<string, unknown>; // optional context for the transformation
|
|
40
|
-
PROMPT?: Record<string, unknown>; // what the user provides as input
|
|
41
|
-
ANSWER?: Record<string, unknown>; // what the model should return as output
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ExampleFileContent {
|
|
45
|
-
examples: TransformationExample[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface AITransformerOptions {
|
|
49
|
-
modelName?: string; // The Gemini model to use
|
|
50
|
-
systemInstructions?: string; // Custom system instructions for the model
|
|
51
|
-
chatConfig?: ChatConfig; // Configuration object for the chat session
|
|
52
|
-
examplesFile?: string; // Path to JSON file containing transformation examples
|
|
53
|
-
exampleData?: TransformationExample[]; // Inline examples to seed the transformer
|
|
54
|
-
sourceKey?: string; // Key name for source data in examples
|
|
55
|
-
targetKey?: string; // Key name for target data in examples
|
|
56
|
-
contextKey?: string; // Key name for context data in examples
|
|
57
|
-
maxRetries?: number; // Maximum retry attempts for auto-retry functionality
|
|
58
|
-
retryDelay?: number; // Initial retry delay in milliseconds
|
|
59
|
-
// ? https://ai.google.dev/gemini-api/docs/structured-output
|
|
60
|
-
responseSchema?: Object; // Schema for validating model responses
|
|
61
|
-
apiKey?: string; // API key for Google GenAI
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Async validator function type
|
|
65
|
-
export type AsyncValidatorFunction = (payload: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export declare class AITransformer implements AITransformerContext {}
|