ak-gemini 1.0.52 → 1.0.54
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 +231 -79
- package/index.js +341 -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,49 @@ 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
|
+
const instructionExample = examples.find((ex) => ex[this.systemInstructionsKey]);
|
|
182
|
+
if (instructionExample) {
|
|
183
|
+
logger_default.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
184
|
+
this.systemInstructions = instructionExample[this.systemInstructionsKey];
|
|
185
|
+
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
186
|
+
await this.init(true);
|
|
187
|
+
}
|
|
166
188
|
logger_default.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
167
189
|
const historyToAdd = [];
|
|
168
190
|
for (const example of examples) {
|
|
169
191
|
const contextValue = example[this.contextKey] || "";
|
|
170
192
|
const promptValue = example[this.promptKey] || "";
|
|
171
193
|
const answerValue = example[this.answerKey] || "";
|
|
194
|
+
const explanationValue = example[this.explanationKey] || "";
|
|
195
|
+
let userText = "";
|
|
196
|
+
let modelResponse = {};
|
|
172
197
|
if (contextValue) {
|
|
173
|
-
let contextText =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
historyToAdd.push({
|
|
179
|
-
role: "model",
|
|
180
|
-
parts: [{ text: "I understand the context." }]
|
|
181
|
-
});
|
|
198
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
199
|
+
userText += `CONTEXT:
|
|
200
|
+
${contextText}
|
|
201
|
+
|
|
202
|
+
`;
|
|
182
203
|
}
|
|
183
204
|
if (promptValue) {
|
|
184
|
-
let promptText =
|
|
185
|
-
|
|
205
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
206
|
+
userText += promptText;
|
|
186
207
|
}
|
|
187
|
-
if (answerValue)
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
209
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
210
|
+
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
211
|
+
if (userText.trim().length && modelText.trim().length > 0) {
|
|
212
|
+
historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
|
|
213
|
+
historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
|
|
190
214
|
}
|
|
191
215
|
}
|
|
192
216
|
const currentHistory = this?.chat?.getHistory() || [];
|
|
@@ -197,77 +221,64 @@ async function seedWithExamples(examples) {
|
|
|
197
221
|
history: [...currentHistory, ...historyToAdd]
|
|
198
222
|
});
|
|
199
223
|
logger_default.debug("Transformation examples seeded successfully.");
|
|
224
|
+
return this.chat.getHistory();
|
|
200
225
|
}
|
|
201
|
-
async function
|
|
226
|
+
async function rawMessage(sourcePayload) {
|
|
202
227
|
if (!this.chat) {
|
|
203
|
-
throw new Error("Chat session not initialized.
|
|
228
|
+
throw new Error("Chat session not initialized.");
|
|
204
229
|
}
|
|
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.");
|
|
230
|
+
const actualPayload = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
|
|
210
231
|
try {
|
|
211
|
-
result = await this.chat.sendMessage({ message: actualPayload });
|
|
232
|
+
const result = await this.chat.sendMessage({ message: actualPayload });
|
|
233
|
+
const modelResponse = result.text;
|
|
234
|
+
const extractedJSON = extractJSON(modelResponse);
|
|
235
|
+
if (extractedJSON?.data) {
|
|
236
|
+
return extractedJSON.data;
|
|
237
|
+
}
|
|
238
|
+
return extractedJSON;
|
|
212
239
|
} catch (error) {
|
|
213
|
-
|
|
240
|
+
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
241
|
+
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
242
|
+
}
|
|
214
243
|
throw new Error(`Transformation failed: ${error.message}`);
|
|
215
244
|
}
|
|
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
245
|
}
|
|
225
|
-
async function
|
|
246
|
+
async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
247
|
+
if (!this.chat) {
|
|
248
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
249
|
+
}
|
|
226
250
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
227
251
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
228
|
-
let lastPayload = null;
|
|
229
252
|
let lastError = null;
|
|
253
|
+
let lastPayload = null;
|
|
254
|
+
if (sourcePayload && isJSON(sourcePayload)) {
|
|
255
|
+
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
256
|
+
} else if (typeof sourcePayload === "string") {
|
|
257
|
+
lastPayload = sourcePayload;
|
|
258
|
+
} else {
|
|
259
|
+
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
260
|
+
}
|
|
230
261
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
231
262
|
try {
|
|
232
|
-
const transformedPayload = attempt === 0 ? await this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
263
|
+
const transformedPayload = attempt === 0 ? await this.rawMessage(lastPayload) : await this.rebuild(lastPayload, lastError.message);
|
|
264
|
+
lastPayload = transformedPayload;
|
|
265
|
+
if (validatorFn) {
|
|
266
|
+
await validatorFn(transformedPayload);
|
|
267
|
+
}
|
|
268
|
+
logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
269
|
+
return transformedPayload;
|
|
236
270
|
} catch (error) {
|
|
237
271
|
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}`);
|
|
272
|
+
logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
273
|
+
if (attempt >= maxRetries) {
|
|
274
|
+
logger_default.error(`All ${maxRetries + 1} attempts failed.`);
|
|
275
|
+
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
248
276
|
}
|
|
277
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
278
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
249
279
|
}
|
|
250
280
|
}
|
|
251
281
|
}
|
|
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
282
|
async function rebuildPayload(lastPayload, serverError) {
|
|
272
283
|
await this.init();
|
|
273
284
|
const prompt = `
|
|
@@ -277,6 +288,7 @@ The server's error message is quoted afterward.
|
|
|
277
288
|
---------------- BAD PAYLOAD ----------------
|
|
278
289
|
${JSON.stringify(lastPayload, null, 2)}
|
|
279
290
|
|
|
291
|
+
|
|
280
292
|
---------------- SERVER ERROR ----------------
|
|
281
293
|
${serverError}
|
|
282
294
|
|
|
@@ -296,6 +308,25 @@ Respond with JSON only \u2013 no comments or explanations.
|
|
|
296
308
|
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
297
309
|
}
|
|
298
310
|
}
|
|
311
|
+
async function estimateTokenUsage(nextPayload) {
|
|
312
|
+
const contents = [];
|
|
313
|
+
if (this.systemInstructions) {
|
|
314
|
+
contents.push({ parts: [{ text: this.systemInstructions }] });
|
|
315
|
+
}
|
|
316
|
+
if (this.chat && typeof this.chat.getHistory === "function") {
|
|
317
|
+
const history = this.chat.getHistory();
|
|
318
|
+
if (Array.isArray(history) && history.length > 0) {
|
|
319
|
+
contents.push(...history);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
|
|
323
|
+
contents.push({ parts: [{ text: nextMessage }] });
|
|
324
|
+
const resp = await this.genAIClient.models.countTokens({
|
|
325
|
+
model: this.modelName,
|
|
326
|
+
contents
|
|
327
|
+
});
|
|
328
|
+
return resp;
|
|
329
|
+
}
|
|
299
330
|
async function resetChat() {
|
|
300
331
|
if (this.chat) {
|
|
301
332
|
logger_default.debug("Resetting Gemini chat session...");
|
|
@@ -317,6 +348,128 @@ function getChatHistory() {
|
|
|
317
348
|
}
|
|
318
349
|
return this.chat.getHistory();
|
|
319
350
|
}
|
|
351
|
+
function isJSON(data) {
|
|
352
|
+
try {
|
|
353
|
+
const attempt = JSON.stringify(data);
|
|
354
|
+
if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
|
|
355
|
+
if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function isJSONStr(string) {
|
|
365
|
+
if (typeof string !== "string") return false;
|
|
366
|
+
try {
|
|
367
|
+
const result = JSON.parse(string);
|
|
368
|
+
const type = Object.prototype.toString.call(result);
|
|
369
|
+
return type === "[object Object]" || type === "[object Array]";
|
|
370
|
+
} catch (err) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function extractJSON(text) {
|
|
375
|
+
if (!text || typeof text !== "string") {
|
|
376
|
+
throw new Error("No text provided for JSON extraction");
|
|
377
|
+
}
|
|
378
|
+
if (isJSONStr(text.trim())) {
|
|
379
|
+
return JSON.parse(text.trim());
|
|
380
|
+
}
|
|
381
|
+
const codeBlockPatterns = [
|
|
382
|
+
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
383
|
+
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
384
|
+
];
|
|
385
|
+
for (const pattern of codeBlockPatterns) {
|
|
386
|
+
const matches = text.match(pattern);
|
|
387
|
+
if (matches) {
|
|
388
|
+
for (const match of matches) {
|
|
389
|
+
const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
|
|
390
|
+
if (isJSONStr(jsonContent)) {
|
|
391
|
+
return JSON.parse(jsonContent);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const jsonPatterns = [
|
|
397
|
+
// Match complete JSON objects
|
|
398
|
+
/\{[\s\S]*\}/g,
|
|
399
|
+
// Match complete JSON arrays
|
|
400
|
+
/\[[\s\S]*\]/g
|
|
401
|
+
];
|
|
402
|
+
for (const pattern of jsonPatterns) {
|
|
403
|
+
const matches = text.match(pattern);
|
|
404
|
+
if (matches) {
|
|
405
|
+
for (const match of matches) {
|
|
406
|
+
const candidate = match.trim();
|
|
407
|
+
if (isJSONStr(candidate)) {
|
|
408
|
+
return JSON.parse(candidate);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const advancedExtract = findCompleteJSONStructures(text);
|
|
414
|
+
if (advancedExtract.length > 0) {
|
|
415
|
+
for (const candidate of advancedExtract) {
|
|
416
|
+
if (isJSONStr(candidate)) {
|
|
417
|
+
return JSON.parse(candidate);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
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();
|
|
422
|
+
if (isJSONStr(cleanedText)) {
|
|
423
|
+
return JSON.parse(cleanedText);
|
|
424
|
+
}
|
|
425
|
+
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
426
|
+
}
|
|
427
|
+
function findCompleteJSONStructures(text) {
|
|
428
|
+
const results = [];
|
|
429
|
+
const startChars = ["{", "["];
|
|
430
|
+
for (let i = 0; i < text.length; i++) {
|
|
431
|
+
if (startChars.includes(text[i])) {
|
|
432
|
+
const extracted = extractCompleteStructure(text, i);
|
|
433
|
+
if (extracted) {
|
|
434
|
+
results.push(extracted);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return results;
|
|
439
|
+
}
|
|
440
|
+
function extractCompleteStructure(text, startPos) {
|
|
441
|
+
const startChar = text[startPos];
|
|
442
|
+
const endChar = startChar === "{" ? "}" : "]";
|
|
443
|
+
let depth = 0;
|
|
444
|
+
let inString = false;
|
|
445
|
+
let escaped = false;
|
|
446
|
+
for (let i = startPos; i < text.length; i++) {
|
|
447
|
+
const char = text[i];
|
|
448
|
+
if (escaped) {
|
|
449
|
+
escaped = false;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (char === "\\" && inString) {
|
|
453
|
+
escaped = true;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === '"' && !escaped) {
|
|
457
|
+
inString = !inString;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (!inString) {
|
|
461
|
+
if (char === startChar) {
|
|
462
|
+
depth++;
|
|
463
|
+
} else if (char === endChar) {
|
|
464
|
+
depth--;
|
|
465
|
+
if (depth === 0) {
|
|
466
|
+
return text.substring(startPos, i + 1);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
320
473
|
if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
321
474
|
logger_default.info("RUNNING AI Transformer as standalone script...");
|
|
322
475
|
(async () => {
|
|
@@ -361,7 +514,7 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
361
514
|
}
|
|
362
515
|
return payload;
|
|
363
516
|
};
|
|
364
|
-
const validatedResponse = await transformer.
|
|
517
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
365
518
|
{ "name": "Lynn" },
|
|
366
519
|
mockValidator
|
|
367
520
|
);
|
|
@@ -375,6 +528,5 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
375
528
|
}
|
|
376
529
|
// Annotate the CommonJS export names for ESM import in node:
|
|
377
530
|
0 && (module.exports = {
|
|
378
|
-
AITransformer,
|
|
379
531
|
log
|
|
380
532
|
});
|
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,26 @@ 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
|
+
const instructionExample = examples.find(ex => ex[this.systemInstructionsKey]);
|
|
228
|
+
if (instructionExample) {
|
|
229
|
+
log.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
230
|
+
this.systemInstructions = instructionExample[this.systemInstructionsKey];
|
|
231
|
+
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
232
|
+
await this.init(true); // Reinitialize chat with new system instructions
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
log.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
199
236
|
const historyToAdd = [];
|
|
200
237
|
|
|
@@ -203,31 +240,31 @@ async function seedWithExamples(examples) {
|
|
|
203
240
|
const contextValue = example[this.contextKey] || "";
|
|
204
241
|
const promptValue = example[this.promptKey] || "";
|
|
205
242
|
const answerValue = example[this.answerKey] || "";
|
|
243
|
+
const explanationValue = example[this.explanationKey] || "";
|
|
244
|
+
let userText = "";
|
|
245
|
+
let modelResponse = {};
|
|
206
246
|
|
|
207
247
|
// Add context as user message with special formatting to make it part of the example flow
|
|
208
248
|
if (contextValue) {
|
|
209
|
-
let contextText =
|
|
249
|
+
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
210
250
|
// 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
|
-
});
|
|
251
|
+
userText += `CONTEXT:\n${contextText}\n\n`;
|
|
220
252
|
}
|
|
221
253
|
|
|
222
254
|
if (promptValue) {
|
|
223
|
-
let promptText =
|
|
224
|
-
|
|
255
|
+
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
256
|
+
userText += promptText;
|
|
225
257
|
}
|
|
226
258
|
|
|
227
|
-
if (answerValue)
|
|
228
|
-
|
|
229
|
-
|
|
259
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
260
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
261
|
+
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
262
|
+
|
|
263
|
+
if (userText.trim().length && modelText.trim().length > 0) {
|
|
264
|
+
historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
|
|
265
|
+
historyToAdd.push({ role: 'model', parts: [{ text: modelText.trim() }] });
|
|
230
266
|
}
|
|
267
|
+
|
|
231
268
|
}
|
|
232
269
|
|
|
233
270
|
const currentHistory = this?.chat?.getHistory() || [];
|
|
@@ -240,6 +277,8 @@ async function seedWithExamples(examples) {
|
|
|
240
277
|
});
|
|
241
278
|
|
|
242
279
|
log.debug("Transformation examples seeded successfully.");
|
|
280
|
+
|
|
281
|
+
return this.chat.getHistory(); // Return the updated chat history for reference
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
/**
|
|
@@ -248,90 +287,150 @@ async function seedWithExamples(examples) {
|
|
|
248
287
|
* @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
|
|
249
288
|
* @throws {Error} If the transformation fails or returns invalid JSON.
|
|
250
289
|
*/
|
|
251
|
-
|
|
290
|
+
/**
|
|
291
|
+
* (Internal) Sends a single prompt to the model and parses the response.
|
|
292
|
+
* No validation or retry logic.
|
|
293
|
+
* @this {ExportedAPI}
|
|
294
|
+
* @param {Object|string} sourcePayload - The source payload.
|
|
295
|
+
* @returns {Promise<Object>} - The transformed payload.
|
|
296
|
+
*/
|
|
297
|
+
async function rawMessage(sourcePayload) {
|
|
252
298
|
if (!this.chat) {
|
|
253
|
-
throw new Error("Chat session not initialized.
|
|
299
|
+
throw new Error("Chat session not initialized.");
|
|
254
300
|
}
|
|
255
301
|
|
|
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.");
|
|
302
|
+
const actualPayload = typeof sourcePayload === 'string'
|
|
303
|
+
? sourcePayload
|
|
304
|
+
: JSON.stringify(sourcePayload, null, 2);
|
|
261
305
|
|
|
262
306
|
try {
|
|
263
|
-
result = await this.chat.sendMessage({ message: actualPayload });
|
|
307
|
+
const result = await this.chat.sendMessage({ message: actualPayload });
|
|
308
|
+
const modelResponse = result.text;
|
|
309
|
+
const extractedJSON = extractJSON(modelResponse); // Assuming extractJSON is defined
|
|
310
|
+
|
|
311
|
+
// Unwrap the 'data' property if it exists
|
|
312
|
+
if (extractedJSON?.data) {
|
|
313
|
+
return extractedJSON.data;
|
|
314
|
+
}
|
|
315
|
+
return extractedJSON;
|
|
316
|
+
|
|
264
317
|
} catch (error) {
|
|
265
|
-
|
|
318
|
+
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
319
|
+
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
320
|
+
}
|
|
321
|
+
// For other API errors, just re-throw
|
|
266
322
|
throw new Error(`Transformation failed: ${error.message}`);
|
|
267
323
|
}
|
|
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
324
|
}
|
|
278
325
|
|
|
279
326
|
/**
|
|
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
|
|
327
|
+
* (Engine) Transforms a payload with validation and automatic retry logic.
|
|
328
|
+
* @this {ExportedAPI}
|
|
329
|
+
* @param {Object} sourcePayload - The source payload to transform.
|
|
330
|
+
* @param {Object} [options] - Options for the validation process.
|
|
331
|
+
* @param {AsyncValidatorFunction | null} validatorFn - The specific validator to use for this run.
|
|
332
|
+
* @returns {Promise<Object>} - The validated transformed payload.
|
|
288
333
|
*/
|
|
289
|
-
async function
|
|
334
|
+
async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
335
|
+
if (!this.chat) {
|
|
336
|
+
throw new Error("Chat session not initialized. Please call init() first.");
|
|
337
|
+
}
|
|
290
338
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
291
339
|
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
292
340
|
|
|
293
|
-
let lastPayload = null;
|
|
294
341
|
let lastError = null;
|
|
342
|
+
let lastPayload = null; // Store the payload that caused the validation error
|
|
343
|
+
|
|
344
|
+
// Prepare the payload
|
|
345
|
+
if (sourcePayload && isJSON(sourcePayload)) {
|
|
346
|
+
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
347
|
+
} else if (typeof sourcePayload === 'string') {
|
|
348
|
+
lastPayload = sourcePayload;
|
|
349
|
+
} else {
|
|
350
|
+
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
351
|
+
}
|
|
295
352
|
|
|
296
353
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
297
354
|
try {
|
|
298
|
-
//
|
|
299
|
-
const transformedPayload = attempt === 0
|
|
300
|
-
? await this.
|
|
355
|
+
// Step 1: Get the transformed payload
|
|
356
|
+
const transformedPayload = (attempt === 0)
|
|
357
|
+
? await this.rawMessage(lastPayload) // Use the new raw method
|
|
301
358
|
: await this.rebuild(lastPayload, lastError.message);
|
|
302
359
|
|
|
303
|
-
//
|
|
304
|
-
|
|
360
|
+
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
361
|
+
|
|
362
|
+
// Step 2: Validate if a validator is provided
|
|
363
|
+
if (validatorFn) {
|
|
364
|
+
await validatorFn(transformedPayload); // Validator throws on failure
|
|
365
|
+
}
|
|
305
366
|
|
|
306
|
-
|
|
307
|
-
|
|
367
|
+
// Step 3: Success!
|
|
368
|
+
log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
369
|
+
return transformedPayload;
|
|
308
370
|
|
|
309
371
|
} catch (error) {
|
|
310
372
|
lastError = error;
|
|
373
|
+
log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
311
374
|
|
|
312
|
-
if (attempt
|
|
313
|
-
|
|
314
|
-
|
|
375
|
+
if (attempt >= maxRetries) {
|
|
376
|
+
log.error(`All ${maxRetries + 1} attempts failed.`);
|
|
377
|
+
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
315
378
|
}
|
|
316
379
|
|
|
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
|
-
}
|
|
380
|
+
// Wait before retrying
|
|
381
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
382
|
+
await new Promise(res => setTimeout(res, delay));
|
|
325
383
|
}
|
|
326
384
|
}
|
|
327
385
|
}
|
|
328
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Rebuilds a payload based on server error feedback
|
|
389
|
+
* @param {Object} lastPayload - The payload that failed validation
|
|
390
|
+
* @param {string} serverError - The error message from the server
|
|
391
|
+
* @returns {Promise<Object>} - A new corrected payload
|
|
392
|
+
* @throws {Error} If the rebuild process fails.
|
|
393
|
+
*/
|
|
394
|
+
async function rebuildPayload(lastPayload, serverError) {
|
|
395
|
+
await this.init(); // Ensure chat is initialized
|
|
396
|
+
const prompt = `
|
|
397
|
+
The previous JSON payload (below) failed validation.
|
|
398
|
+
The server's error message is quoted afterward.
|
|
399
|
+
|
|
400
|
+
---------------- BAD PAYLOAD ----------------
|
|
401
|
+
${JSON.stringify(lastPayload, null, 2)}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
---------------- SERVER ERROR ----------------
|
|
405
|
+
${serverError}
|
|
406
|
+
|
|
407
|
+
Please return a NEW JSON payload that corrects the issue.
|
|
408
|
+
Respond with JSON only – no comments or explanations.
|
|
409
|
+
`;
|
|
410
|
+
|
|
411
|
+
let result;
|
|
412
|
+
try {
|
|
413
|
+
result = await this.chat.sendMessage({ message: prompt });
|
|
414
|
+
} catch (err) {
|
|
415
|
+
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const text = result.text ?? result.response ?? '';
|
|
420
|
+
return typeof text === 'object' ? text : JSON.parse(text);
|
|
421
|
+
} catch (parseErr) {
|
|
422
|
+
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
329
428
|
|
|
330
429
|
/**
|
|
331
430
|
* Estimate total token usage if you were to send a new payload as the next message.
|
|
332
431
|
* Considers system instructions, current chat history (including examples), and the new message.
|
|
333
432
|
* @param {object|string} nextPayload - The next user message to be sent (object or string)
|
|
334
|
-
* @returns {Promise<{ totalTokens: number
|
|
433
|
+
* @returns {Promise<{ totalTokens: number }>} - The result of Gemini's countTokens API
|
|
335
434
|
*/
|
|
336
435
|
async function estimateTokenUsage(nextPayload) {
|
|
337
436
|
// Compose the conversation contents, Gemini-style
|
|
@@ -367,44 +466,6 @@ async function estimateTokenUsage(nextPayload) {
|
|
|
367
466
|
return resp; // includes totalTokens, possibly breakdown
|
|
368
467
|
}
|
|
369
468
|
|
|
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
469
|
|
|
409
470
|
/**
|
|
410
471
|
* Resets the current chat session, clearing all history and examples
|
|
@@ -439,6 +500,170 @@ function getChatHistory() {
|
|
|
439
500
|
}
|
|
440
501
|
|
|
441
502
|
|
|
503
|
+
/*
|
|
504
|
+
----
|
|
505
|
+
HELPERS
|
|
506
|
+
----
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
function isJSON(data) {
|
|
510
|
+
try {
|
|
511
|
+
const attempt = JSON.stringify(data);
|
|
512
|
+
if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
|
|
513
|
+
if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
} catch (e) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isJSONStr(string) {
|
|
524
|
+
if (typeof string !== 'string') return false;
|
|
525
|
+
try {
|
|
526
|
+
const result = JSON.parse(string);
|
|
527
|
+
const type = Object.prototype.toString.call(result);
|
|
528
|
+
return type === '[object Object]' || type === '[object Array]';
|
|
529
|
+
} catch (err) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function extractJSON(text) {
|
|
535
|
+
if (!text || typeof text !== 'string') {
|
|
536
|
+
throw new Error('No text provided for JSON extraction');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Strategy 1: Try parsing the entire response as JSON
|
|
540
|
+
if (isJSONStr(text.trim())) {
|
|
541
|
+
return JSON.parse(text.trim());
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Strategy 2: Look for JSON code blocks (```json...``` or ```...```)
|
|
545
|
+
const codeBlockPatterns = [
|
|
546
|
+
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
547
|
+
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
for (const pattern of codeBlockPatterns) {
|
|
551
|
+
const matches = text.match(pattern);
|
|
552
|
+
if (matches) {
|
|
553
|
+
for (const match of matches) {
|
|
554
|
+
const jsonContent = match.replace(/```json\s*\n?/gi, '').replace(/```\s*\n?/gi, '').trim();
|
|
555
|
+
if (isJSONStr(jsonContent)) {
|
|
556
|
+
return JSON.parse(jsonContent);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Strategy 3: Look for JSON objects/arrays using bracket matching
|
|
563
|
+
const jsonPatterns = [
|
|
564
|
+
// Match complete JSON objects
|
|
565
|
+
/\{[\s\S]*\}/g,
|
|
566
|
+
// Match complete JSON arrays
|
|
567
|
+
/\[[\s\S]*\]/g
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
for (const pattern of jsonPatterns) {
|
|
571
|
+
const matches = text.match(pattern);
|
|
572
|
+
if (matches) {
|
|
573
|
+
for (const match of matches) {
|
|
574
|
+
const candidate = match.trim();
|
|
575
|
+
if (isJSONStr(candidate)) {
|
|
576
|
+
return JSON.parse(candidate);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Strategy 4: Advanced bracket matching for nested structures
|
|
583
|
+
const advancedExtract = findCompleteJSONStructures(text);
|
|
584
|
+
if (advancedExtract.length > 0) {
|
|
585
|
+
// Return the first valid JSON structure found
|
|
586
|
+
for (const candidate of advancedExtract) {
|
|
587
|
+
if (isJSONStr(candidate)) {
|
|
588
|
+
return JSON.parse(candidate);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Strategy 5: Clean up common formatting issues and retry
|
|
594
|
+
const cleanedText = text
|
|
595
|
+
.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, '') // Remove conversational intros
|
|
596
|
+
.replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, '')
|
|
597
|
+
.replace(/^\s*The\s+.*?is\s*[:\n]/gi, '')
|
|
598
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* comments */
|
|
599
|
+
.replace(/\/\/.*$/gm, '') // Remove // comments
|
|
600
|
+
.trim();
|
|
601
|
+
|
|
602
|
+
if (isJSONStr(cleanedText)) {
|
|
603
|
+
return JSON.parse(cleanedText);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// If all else fails, throw an error with helpful information
|
|
607
|
+
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function findCompleteJSONStructures(text) {
|
|
611
|
+
const results = [];
|
|
612
|
+
const startChars = ['{', '['];
|
|
613
|
+
|
|
614
|
+
for (let i = 0; i < text.length; i++) {
|
|
615
|
+
if (startChars.includes(text[i])) {
|
|
616
|
+
const extracted = extractCompleteStructure(text, i);
|
|
617
|
+
if (extracted) {
|
|
618
|
+
results.push(extracted);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return results;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
function extractCompleteStructure(text, startPos) {
|
|
628
|
+
const startChar = text[startPos];
|
|
629
|
+
const endChar = startChar === '{' ? '}' : ']';
|
|
630
|
+
let depth = 0;
|
|
631
|
+
let inString = false;
|
|
632
|
+
let escaped = false;
|
|
633
|
+
|
|
634
|
+
for (let i = startPos; i < text.length; i++) {
|
|
635
|
+
const char = text[i];
|
|
636
|
+
|
|
637
|
+
if (escaped) {
|
|
638
|
+
escaped = false;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (char === '\\' && inString) {
|
|
643
|
+
escaped = true;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (char === '"' && !escaped) {
|
|
648
|
+
inString = !inString;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (!inString) {
|
|
653
|
+
if (char === startChar) {
|
|
654
|
+
depth++;
|
|
655
|
+
} else if (char === endChar) {
|
|
656
|
+
depth--;
|
|
657
|
+
if (depth === 0) {
|
|
658
|
+
return text.substring(startPos, i + 1);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return null; // Incomplete structure
|
|
665
|
+
}
|
|
666
|
+
|
|
442
667
|
if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
443
668
|
log.info("RUNNING AI Transformer as standalone script...");
|
|
444
669
|
(
|
|
@@ -490,7 +715,7 @@ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
|
490
715
|
return payload; // Return the payload if validation passes
|
|
491
716
|
};
|
|
492
717
|
|
|
493
|
-
const validatedResponse = await transformer.
|
|
718
|
+
const validatedResponse = await transformer.messageAndValidate(
|
|
494
719
|
{ "name": "Lynn" },
|
|
495
720
|
mockValidator
|
|
496
721
|
);
|