ak-gemini 1.0.52 → 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 +340 -116
- package/package.json +1 -1
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 = {
|
|
@@ -65,7 +65,7 @@ const DEFAULT_CHAT_CONFIG = {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* @typedef {import('./types').AITransformer}
|
|
68
|
+
* @typedef {import('./types').AITransformer} AITransformerUtility
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
|
|
@@ -73,12 +73,11 @@ const DEFAULT_CHAT_CONFIG = {
|
|
|
73
73
|
/**
|
|
74
74
|
* main export class for AI Transformer
|
|
75
75
|
* @class AITransformer
|
|
76
|
-
* @type {
|
|
76
|
+
* @type {AITransformerUtility}
|
|
77
77
|
* @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
|
|
78
78
|
* @implements {ExportedAPI}
|
|
79
79
|
*/
|
|
80
|
-
|
|
81
|
-
export default class AITransformer {
|
|
80
|
+
class AITransformer {
|
|
82
81
|
/**
|
|
83
82
|
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
84
83
|
*
|
|
@@ -88,25 +87,40 @@ export default class AITransformer {
|
|
|
88
87
|
this.promptKey = "";
|
|
89
88
|
this.answerKey = "";
|
|
90
89
|
this.contextKey = "";
|
|
90
|
+
this.explanationKey = "";
|
|
91
|
+
this.systemInstructionKey = "";
|
|
91
92
|
this.maxRetries = 3;
|
|
92
93
|
this.retryDelay = 1000;
|
|
93
94
|
this.systemInstructions = "";
|
|
94
95
|
this.chatConfig = {};
|
|
95
96
|
this.apiKey = GEMINI_API_KEY;
|
|
97
|
+
this.onlyJSON = true; // always return JSON
|
|
98
|
+
this.asyncValidator = null; // for transformWithValidation
|
|
96
99
|
AITransformFactory.call(this, options);
|
|
97
100
|
|
|
98
101
|
//external API
|
|
99
102
|
this.init = initChat.bind(this);
|
|
100
103
|
this.seed = seedWithExamples.bind(this);
|
|
101
|
-
|
|
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
|
+
|
|
102
114
|
this.rebuild = rebuildPayload.bind(this);
|
|
103
115
|
this.reset = resetChat.bind(this);
|
|
104
116
|
this.getHistory = getChatHistory.bind(this);
|
|
105
|
-
this.
|
|
117
|
+
this.messageAndValidate = prepareAndValidateMessage.bind(this);
|
|
106
118
|
this.estimate = estimateTokenUsage.bind(this);
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
|
-
|
|
121
|
+
|
|
122
|
+
export default AITransformer;
|
|
123
|
+
|
|
110
124
|
/**
|
|
111
125
|
* factory function to create an AI Transformer instance
|
|
112
126
|
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
@@ -117,7 +131,7 @@ function AITransformFactory(options = {}) {
|
|
|
117
131
|
this.modelName = options.modelName || 'gemini-2.0-flash';
|
|
118
132
|
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
119
133
|
|
|
120
|
-
this.apiKey = options.apiKey
|
|
134
|
+
this.apiKey = options.apiKey !== undefined && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
|
|
121
135
|
if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
|
|
122
136
|
// Build chat config, making sure systemInstruction uses the custom instructions
|
|
123
137
|
this.chatConfig = {
|
|
@@ -136,14 +150,22 @@ function AITransformFactory(options = {}) {
|
|
|
136
150
|
this.exampleData = options.exampleData || null; // can be used instead of examplesFile
|
|
137
151
|
|
|
138
152
|
// Use configurable keys with fallbacks
|
|
139
|
-
this.promptKey = options.
|
|
140
|
-
this.answerKey = options.
|
|
141
|
-
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
|
|
142
158
|
|
|
143
159
|
// Retry configuration
|
|
144
160
|
this.maxRetries = options.maxRetries || 3;
|
|
145
161
|
this.retryDelay = options.retryDelay || 1000;
|
|
146
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
|
+
|
|
147
169
|
if (this.promptKey === this.answerKey) {
|
|
148
170
|
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
149
171
|
}
|
|
@@ -158,11 +180,12 @@ function AITransformFactory(options = {}) {
|
|
|
158
180
|
|
|
159
181
|
/**
|
|
160
182
|
* Initializes the chat session with the specified model and configurations.
|
|
183
|
+
* @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
|
|
161
184
|
* @this {ExportedAPI}
|
|
162
185
|
* @returns {Promise<void>}
|
|
163
186
|
*/
|
|
164
|
-
async function initChat() {
|
|
165
|
-
if (this.chat) return;
|
|
187
|
+
async function initChat(force = false) {
|
|
188
|
+
if (this.chat && !force) return;
|
|
166
189
|
|
|
167
190
|
log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
168
191
|
|
|
@@ -180,6 +203,7 @@ async function initChat() {
|
|
|
180
203
|
* Seeds the chat session with example transformations.
|
|
181
204
|
* @this {ExportedAPI}
|
|
182
205
|
* @param {TransformationExample[]} [examples] - An array of transformation examples.
|
|
206
|
+
* @this {ExportedAPI}
|
|
183
207
|
* @returns {Promise<void>}
|
|
184
208
|
*/
|
|
185
209
|
async function seedWithExamples(examples) {
|
|
@@ -188,13 +212,25 @@ async function seedWithExamples(examples) {
|
|
|
188
212
|
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
189
213
|
if (this.examplesFile) {
|
|
190
214
|
log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
191
|
-
|
|
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
|
+
}
|
|
192
221
|
} else {
|
|
193
222
|
log.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
194
223
|
return;
|
|
195
224
|
}
|
|
196
225
|
}
|
|
197
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
|
+
|
|
198
234
|
log.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
199
235
|
const historyToAdd = [];
|
|
200
236
|
|
|
@@ -203,31 +239,31 @@ async function seedWithExamples(examples) {
|
|
|
203
239
|
const contextValue = example[this.contextKey] || "";
|
|
204
240
|
const promptValue = example[this.promptKey] || "";
|
|
205
241
|
const answerValue = example[this.answerKey] || "";
|
|
242
|
+
const explanationValue = example[this.explanationKey] || "";
|
|
243
|
+
let userText = "";
|
|
244
|
+
let modelResponse = {};
|
|
206
245
|
|
|
207
246
|
// Add context as user message with special formatting to make it part of the example flow
|
|
208
247
|
if (contextValue) {
|
|
209
|
-
let contextText =
|
|
248
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
210
249
|
// Prefix context to make it clear it's contextual information
|
|
211
|
-
|
|
212
|
-
role: 'user',
|
|
213
|
-
parts: [{ text: `Context: ${contextText}` }]
|
|
214
|
-
});
|
|
215
|
-
// Add a brief model acknowledgment
|
|
216
|
-
historyToAdd.push({
|
|
217
|
-
role: 'model',
|
|
218
|
-
parts: [{ text: "I understand the context." }]
|
|
219
|
-
});
|
|
250
|
+
userText += `CONTEXT:\n${contextText}\n\n`;
|
|
220
251
|
}
|
|
221
252
|
|
|
222
253
|
if (promptValue) {
|
|
223
|
-
let promptText =
|
|
224
|
-
|
|
254
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
255
|
+
userText += promptText;
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
if (answerValue)
|
|
228
|
-
|
|
229
|
-
|
|
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() }] });
|
|
230
265
|
}
|
|
266
|
+
|
|
231
267
|
}
|
|
232
268
|
|
|
233
269
|
const currentHistory = this?.chat?.getHistory() || [];
|
|
@@ -240,6 +276,8 @@ async function seedWithExamples(examples) {
|
|
|
240
276
|
});
|
|
241
277
|
|
|
242
278
|
log.debug("Transformation examples seeded successfully.");
|
|
279
|
+
|
|
280
|
+
return this.chat.getHistory(); // Return the updated chat history for reference
|
|
243
281
|
}
|
|
244
282
|
|
|
245
283
|
/**
|
|
@@ -248,90 +286,150 @@ async function seedWithExamples(examples) {
|
|
|
248
286
|
* @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
|
|
249
287
|
* @throws {Error} If the transformation fails or returns invalid JSON.
|
|
250
288
|
*/
|
|
251
|
-
|
|
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) {
|
|
252
297
|
if (!this.chat) {
|
|
253
|
-
throw new Error("Chat session not initialized.
|
|
298
|
+
throw new Error("Chat session not initialized.");
|
|
254
299
|
}
|
|
255
300
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
else if (typeof sourcePayload === 'string') actualPayload = sourcePayload;
|
|
260
|
-
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);
|
|
261
304
|
|
|
262
305
|
try {
|
|
263
|
-
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
|
+
|
|
264
316
|
} catch (error) {
|
|
265
|
-
|
|
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
|
|
266
321
|
throw new Error(`Transformation failed: ${error.message}`);
|
|
267
322
|
}
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const modelResponse = result.text;
|
|
271
|
-
const parsedResponse = JSON.parse(modelResponse);
|
|
272
|
-
return parsedResponse;
|
|
273
|
-
} catch (parseError) {
|
|
274
|
-
log.error("Error parsing Gemini response:", parseError);
|
|
275
|
-
throw new Error(`Invalid JSON response from Gemini: ${parseError.message}`);
|
|
276
|
-
}
|
|
277
323
|
}
|
|
278
324
|
|
|
279
325
|
/**
|
|
280
|
-
* Transforms payload with
|
|
281
|
-
* @
|
|
282
|
-
* @param {
|
|
283
|
-
* @param {Object} [options] - Options for the validation process
|
|
284
|
-
* @param {
|
|
285
|
-
* @
|
|
286
|
-
* @returns {Promise<Object>} - The validated transformed payload
|
|
287
|
-
* @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.
|
|
288
332
|
*/
|
|
289
|
-
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
|
+
}
|
|
290
337
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
291
338
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
292
339
|
|
|
293
|
-
let lastPayload = null;
|
|
294
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
|
+
}
|
|
295
351
|
|
|
296
352
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
297
353
|
try {
|
|
298
|
-
//
|
|
299
|
-
const transformedPayload = attempt === 0
|
|
300
|
-
? await this.
|
|
354
|
+
// Step 1: Get the transformed payload
|
|
355
|
+
const transformedPayload = (attempt === 0)
|
|
356
|
+
? await this.rawMessage(lastPayload) // Use the new raw method
|
|
301
357
|
: await this.rebuild(lastPayload, lastError.message);
|
|
302
358
|
|
|
303
|
-
//
|
|
304
|
-
|
|
359
|
+
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
360
|
+
|
|
361
|
+
// Step 2: Validate if a validator is provided
|
|
362
|
+
if (validatorFn) {
|
|
363
|
+
await validatorFn(transformedPayload); // Validator throws on failure
|
|
364
|
+
}
|
|
305
365
|
|
|
306
|
-
|
|
307
|
-
|
|
366
|
+
// Step 3: Success!
|
|
367
|
+
log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
368
|
+
return transformedPayload;
|
|
308
369
|
|
|
309
370
|
} catch (error) {
|
|
310
371
|
lastError = error;
|
|
372
|
+
log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
311
373
|
|
|
312
|
-
if (attempt
|
|
313
|
-
|
|
314
|
-
|
|
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}`);
|
|
315
377
|
}
|
|
316
378
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
await new Promise(res => setTimeout(res, delay));
|
|
321
|
-
} else {
|
|
322
|
-
log.error(`All ${maxRetries + 1} attempts failed`);
|
|
323
|
-
throw new Error(`Transformation with validation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
324
|
-
}
|
|
379
|
+
// Wait before retrying
|
|
380
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
381
|
+
await new Promise(res => setTimeout(res, delay));
|
|
325
382
|
}
|
|
326
383
|
}
|
|
327
384
|
}
|
|
328
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
|
+
|
|
329
427
|
|
|
330
428
|
/**
|
|
331
429
|
* Estimate total token usage if you were to send a new payload as the next message.
|
|
332
430
|
* Considers system instructions, current chat history (including examples), and the new message.
|
|
333
431
|
* @param {object|string} nextPayload - The next user message to be sent (object or string)
|
|
334
|
-
* @returns {Promise<{ totalTokens: number
|
|
432
|
+
* @returns {Promise<{ totalTokens: number }>} - The result of Gemini's countTokens API
|
|
335
433
|
*/
|
|
336
434
|
async function estimateTokenUsage(nextPayload) {
|
|
337
435
|
// Compose the conversation contents, Gemini-style
|
|
@@ -367,44 +465,6 @@ async function estimateTokenUsage(nextPayload) {
|
|
|
367
465
|
return resp; // includes totalTokens, possibly breakdown
|
|
368
466
|
}
|
|
369
467
|
|
|
370
|
-
/**
|
|
371
|
-
* Rebuilds a payload based on server error feedback
|
|
372
|
-
* @param {Object} lastPayload - The payload that failed validation
|
|
373
|
-
* @param {string} serverError - The error message from the server
|
|
374
|
-
* @returns {Promise<Object>} - A new corrected payload
|
|
375
|
-
* @throws {Error} If the rebuild process fails.
|
|
376
|
-
*/
|
|
377
|
-
async function rebuildPayload(lastPayload, serverError) {
|
|
378
|
-
await this.init();
|
|
379
|
-
|
|
380
|
-
const prompt = `
|
|
381
|
-
The previous JSON payload (below) failed validation.
|
|
382
|
-
The server's error message is quoted afterward.
|
|
383
|
-
|
|
384
|
-
---------------- BAD PAYLOAD ----------------
|
|
385
|
-
${JSON.stringify(lastPayload, null, 2)}
|
|
386
|
-
|
|
387
|
-
---------------- SERVER ERROR ----------------
|
|
388
|
-
${serverError}
|
|
389
|
-
|
|
390
|
-
Please return a NEW JSON payload that corrects the issue.
|
|
391
|
-
Respond with JSON only – no comments or explanations.
|
|
392
|
-
`;
|
|
393
|
-
|
|
394
|
-
let result;
|
|
395
|
-
try {
|
|
396
|
-
result = await this.chat.sendMessage({ message: prompt });
|
|
397
|
-
} catch (err) {
|
|
398
|
-
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
const text = result.text ?? result.response ?? '';
|
|
403
|
-
return typeof text === 'object' ? text : JSON.parse(text);
|
|
404
|
-
} catch (parseErr) {
|
|
405
|
-
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
468
|
|
|
409
469
|
/**
|
|
410
470
|
* Resets the current chat session, clearing all history and examples
|
|
@@ -439,6 +499,170 @@ function getChatHistory() {
|
|
|
439
499
|
}
|
|
440
500
|
|
|
441
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
|
+
|
|
442
666
|
if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
443
667
|
log.info("RUNNING AI Transformer as standalone script...");
|
|
444
668
|
(
|
|
@@ -490,7 +714,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
490
714
|
return payload; // Return the payload if validation passes
|
|
491
715
|
};
|
|
492
716
|
|
|
493
|
-
const validatedResponse = await transformer.
|
|
717
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
494
718
|
{ "name": "Lynn" },
|
|
495
719
|
mockValidator
|
|
496
720
|
);
|