ak-gemini 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -0
- package/index.cjs +41668 -0
- package/index.js +451 -0
- package/logger.js +16 -0
- package/package.json +62 -0
- package/types.ts +65 -0
package/index.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Generic AI transformation module that can be configured for different use cases.
|
|
4
|
+
* Supports various models, system instructions, chat configurations, and example datasets.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./types').SafetySetting} SafetySetting
|
|
9
|
+
* @typedef {import('./types').ChatConfig} ChatConfig
|
|
10
|
+
* @typedef {import('./types').TransformationExample} TransformationExample
|
|
11
|
+
* @typedef {import('./types').ExampleFileContent} ExampleFileContent
|
|
12
|
+
* @typedef {import('./types').AITransformerOptions} AITransformerOptions
|
|
13
|
+
* @typedef {import('./types').AsyncValidatorFunction} AsyncValidatorFunction
|
|
14
|
+
* @typedef {import('./types').AITransformerContext} ExportedAPI
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
//env
|
|
19
|
+
import dotenv from 'dotenv';
|
|
20
|
+
dotenv.config();
|
|
21
|
+
const { NODE_ENV = "unknown", GEMINI_API_KEY } = process.env;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
//deps
|
|
26
|
+
import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
27
|
+
import u from 'ak-tools';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import log from './logger.js';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// defaults
|
|
35
|
+
const DEFAULT_SAFETY_SETTINGS = [
|
|
36
|
+
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
|
|
37
|
+
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SYSTEM_INSTRUCTIONS = `
|
|
41
|
+
You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
|
|
42
|
+
|
|
43
|
+
You will be provided with example transformations (Source JSON -> Target JSON).
|
|
44
|
+
|
|
45
|
+
Learn the mapping rules from these examples.
|
|
46
|
+
|
|
47
|
+
When presented with new Source JSON, apply the learned transformation rules to produce a new Target JSON payload.
|
|
48
|
+
|
|
49
|
+
Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
|
|
50
|
+
|
|
51
|
+
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const DEFAULT_CHAT_CONFIG = {
|
|
55
|
+
responseMimeType: 'application/json',
|
|
56
|
+
temperature: 0.2,
|
|
57
|
+
topP: 0.95,
|
|
58
|
+
topK: 64,
|
|
59
|
+
systemInstruction: DEFAULT_SYSTEM_INSTRUCTIONS,
|
|
60
|
+
safetySettings: DEFAULT_SAFETY_SETTINGS
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* main export class for AI Transformer
|
|
65
|
+
* @class AITransformer
|
|
66
|
+
* @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
|
|
67
|
+
* @implements {ExportedAPI}
|
|
68
|
+
*/
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
export default class AITransformer {
|
|
71
|
+
/**
|
|
72
|
+
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
this.modelName = "";
|
|
77
|
+
this.promptKey = "";
|
|
78
|
+
this.answerKey = "";
|
|
79
|
+
this.contextKey = "";
|
|
80
|
+
this.maxRetries = 3;
|
|
81
|
+
this.retryDelay = 1000;
|
|
82
|
+
this.systemInstructions = "";
|
|
83
|
+
this.chatConfig = {};
|
|
84
|
+
this.apiKey = GEMINI_API_KEY;
|
|
85
|
+
AITransformFactory.call(this, options);
|
|
86
|
+
|
|
87
|
+
//external API
|
|
88
|
+
this.init = initChat.bind(this);
|
|
89
|
+
this.seed = seedWithExamples.bind(this);
|
|
90
|
+
this.message = transformJSON.bind(this);
|
|
91
|
+
this.rebuild = rebuildPayload.bind(this);
|
|
92
|
+
this.reset = resetChat.bind(this);
|
|
93
|
+
this.getHistory = getChatHistory.bind(this);
|
|
94
|
+
this.transformWithValidation = transformWithValidation.bind(this);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* factory function to create an AI Transformer instance
|
|
100
|
+
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
101
|
+
* @returns {void} - An instance of AITransformer with initialized properties and methods
|
|
102
|
+
*/
|
|
103
|
+
function AITransformFactory(options = {}) {
|
|
104
|
+
// ? https://ai.google.dev/gemini-api/docs/models
|
|
105
|
+
this.modelName = options.modelName || 'gemini-2.0-flash';
|
|
106
|
+
this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
107
|
+
|
|
108
|
+
this.apiKey = options.apiKey || GEMINI_API_KEY;
|
|
109
|
+
if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
|
|
110
|
+
// Build chat config, making sure systemInstruction uses the custom instructions
|
|
111
|
+
this.chatConfig = {
|
|
112
|
+
...DEFAULT_CHAT_CONFIG,
|
|
113
|
+
...options.chatConfig,
|
|
114
|
+
systemInstruction: this.systemInstructions
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// response schema is optional, but if provided, it should be a valid JSON schema
|
|
118
|
+
if (options.responseSchema) {
|
|
119
|
+
this.chatConfig.responseSchema = options.responseSchema;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// examples file is optional, but if provided, it should contain valid PROMPT and ANSWER keys
|
|
123
|
+
this.examplesFile = options.examplesFile || null;
|
|
124
|
+
this.exampleData = options.exampleData || null; // can be used instead of examplesFile
|
|
125
|
+
|
|
126
|
+
// Use configurable keys with fallbacks
|
|
127
|
+
this.promptKey = options.sourceKey || 'PROMPT';
|
|
128
|
+
this.answerKey = options.targetKey || 'ANSWER';
|
|
129
|
+
this.contextKey = options.contextKey || 'CONTEXT'; // Now configurable
|
|
130
|
+
|
|
131
|
+
// Retry configuration
|
|
132
|
+
this.maxRetries = options.maxRetries || 3;
|
|
133
|
+
this.retryDelay = options.retryDelay || 1000;
|
|
134
|
+
|
|
135
|
+
if (this.promptKey === this.answerKey) {
|
|
136
|
+
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.debug(`Creating AI Transformer with model: ${this.modelName}`);
|
|
140
|
+
log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
141
|
+
|
|
142
|
+
this.genAIClient = new GoogleGenAI({ apiKey: this.apiKey });
|
|
143
|
+
this.chat = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Initializes the chat session with the specified model and configurations.
|
|
148
|
+
* @this {ExportedAPI}
|
|
149
|
+
* @returns {Promise<void>}
|
|
150
|
+
*/
|
|
151
|
+
async function initChat() {
|
|
152
|
+
if (this.chat) return;
|
|
153
|
+
|
|
154
|
+
log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
155
|
+
|
|
156
|
+
this.chat = await this.genAIClient.chats.create({
|
|
157
|
+
model: this.modelName,
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
config: this.chatConfig,
|
|
160
|
+
history: [],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
log.debug("Gemini chat session initialized.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Seeds the chat session with example transformations.
|
|
168
|
+
* @this {ExportedAPI}
|
|
169
|
+
* @param {TransformationExample[]} [examples] - An array of transformation examples.
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async function seedWithExamples(examples) {
|
|
173
|
+
await this.init();
|
|
174
|
+
|
|
175
|
+
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
176
|
+
if (this.examplesFile) {
|
|
177
|
+
log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
178
|
+
examples = await u.load(path.resolve(this.examplesFile), true);
|
|
179
|
+
} else {
|
|
180
|
+
log.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
log.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
186
|
+
const historyToAdd = [];
|
|
187
|
+
|
|
188
|
+
for (const example of examples) {
|
|
189
|
+
// Use the configurable keys from constructor
|
|
190
|
+
const contextValue = example[this.contextKey] || "";
|
|
191
|
+
const promptValue = example[this.promptKey] || "";
|
|
192
|
+
const answerValue = example[this.answerKey] || "";
|
|
193
|
+
|
|
194
|
+
// Add context as user message with special formatting to make it part of the example flow
|
|
195
|
+
if (contextValue) {
|
|
196
|
+
let contextText = u.isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
197
|
+
// Prefix context to make it clear it's contextual information
|
|
198
|
+
historyToAdd.push({
|
|
199
|
+
role: 'user',
|
|
200
|
+
parts: [{ text: `Context: ${contextText}` }]
|
|
201
|
+
});
|
|
202
|
+
// Add a brief model acknowledgment
|
|
203
|
+
historyToAdd.push({
|
|
204
|
+
role: 'model',
|
|
205
|
+
parts: [{ text: "I understand the context." }]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (promptValue) {
|
|
210
|
+
let promptText = u.isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
211
|
+
historyToAdd.push({ role: 'user', parts: [{ text: promptText }] });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (answerValue) {
|
|
215
|
+
let answerText = u.isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : answerValue;
|
|
216
|
+
historyToAdd.push({ role: 'model', parts: [{ text: answerText }] });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const currentHistory = this.chat.getHistory();
|
|
221
|
+
|
|
222
|
+
this.chat = await this.genAIClient.chats.create({
|
|
223
|
+
model: this.modelName,
|
|
224
|
+
// @ts-ignore
|
|
225
|
+
config: this.chatConfig,
|
|
226
|
+
history: [...currentHistory, ...historyToAdd],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
log.debug("Transformation examples seeded successfully.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Transforms a source JSON payload into a target JSON payload
|
|
234
|
+
* @param {Object} sourcePayload - The source payload (as a JavaScript object).
|
|
235
|
+
* @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
|
|
236
|
+
* @throws {Error} If the transformation fails or returns invalid JSON.
|
|
237
|
+
*/
|
|
238
|
+
async function transformJSON(sourcePayload) {
|
|
239
|
+
if (!this.chat) {
|
|
240
|
+
throw new Error("Chat session not initialized. Call initChat() or seedWithExamples() first.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let result;
|
|
244
|
+
let actualPayload;
|
|
245
|
+
if (sourcePayload && u.isJSON(sourcePayload)) actualPayload = JSON.stringify(sourcePayload, null, 2);
|
|
246
|
+
else if (typeof sourcePayload === 'string') actualPayload = sourcePayload;
|
|
247
|
+
else throw new Error("Invalid source payload. Must be a JSON object or a valid JSON string.");
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
result = await this.chat.sendMessage({ message: actualPayload });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
log.error("Error with Gemini API:", error);
|
|
253
|
+
throw new Error(`Transformation failed: ${error.message}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const modelResponse = result.text;
|
|
258
|
+
const parsedResponse = JSON.parse(modelResponse);
|
|
259
|
+
return parsedResponse;
|
|
260
|
+
} catch (parseError) {
|
|
261
|
+
log.error("Error parsing Gemini response:", parseError);
|
|
262
|
+
throw new Error(`Invalid JSON response from Gemini: ${parseError.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Transforms payload with automatic validation and retry logic
|
|
268
|
+
* @param {Object} sourcePayload - The source payload to transform
|
|
269
|
+
* @param {AsyncValidatorFunction} validatorFn - Async function that validates the transformed payload
|
|
270
|
+
* @param {Object} [options] - Options for the validation process
|
|
271
|
+
* @param {number} [options.maxRetries] - Override default max retries
|
|
272
|
+
* @param {number} [options.retryDelay] - Override default retry delay
|
|
273
|
+
* @returns {Promise<Object>} - The validated transformed payload
|
|
274
|
+
* @throws {Error} If transformation or validation fails after all retries
|
|
275
|
+
*/
|
|
276
|
+
async function transformWithValidation(sourcePayload, validatorFn, options = {}) {
|
|
277
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
278
|
+
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
279
|
+
|
|
280
|
+
let lastPayload = null;
|
|
281
|
+
let lastError = null;
|
|
282
|
+
|
|
283
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
284
|
+
try {
|
|
285
|
+
// First attempt uses normal transformation, subsequent attempts use rebuild
|
|
286
|
+
const transformedPayload = attempt === 0
|
|
287
|
+
? await this.message(sourcePayload)
|
|
288
|
+
: await this.rebuild(lastPayload, lastError.message);
|
|
289
|
+
|
|
290
|
+
// Validate the transformed payload
|
|
291
|
+
const validatedPayload = await validatorFn(transformedPayload);
|
|
292
|
+
|
|
293
|
+
log.debug(`Transformation and validation succeeded on attempt ${attempt + 1}`);
|
|
294
|
+
return validatedPayload;
|
|
295
|
+
|
|
296
|
+
} catch (error) {
|
|
297
|
+
lastError = error;
|
|
298
|
+
|
|
299
|
+
if (attempt === 0) {
|
|
300
|
+
// First attempt failed - could be transformation or validation error
|
|
301
|
+
lastPayload = await this.message(sourcePayload).catch(() => null);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (attempt < maxRetries) {
|
|
305
|
+
const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
|
|
306
|
+
log.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error.message);
|
|
307
|
+
await new Promise(res => setTimeout(res, delay));
|
|
308
|
+
} else {
|
|
309
|
+
log.error(`All ${maxRetries + 1} attempts failed`);
|
|
310
|
+
throw new Error(`Transformation with validation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Rebuilds a payload based on server error feedback
|
|
318
|
+
* @param {Object} lastPayload - The payload that failed validation
|
|
319
|
+
* @param {string} serverError - The error message from the server
|
|
320
|
+
* @returns {Promise<Object>} - A new corrected payload
|
|
321
|
+
* @throws {Error} If the rebuild process fails.
|
|
322
|
+
*/
|
|
323
|
+
async function rebuildPayload(lastPayload, serverError) {
|
|
324
|
+
await this.init();
|
|
325
|
+
|
|
326
|
+
const prompt = `
|
|
327
|
+
The previous JSON payload (below) failed validation.
|
|
328
|
+
The server's error message is quoted afterward.
|
|
329
|
+
|
|
330
|
+
---------------- BAD PAYLOAD ----------------
|
|
331
|
+
${JSON.stringify(lastPayload, null, 2)}
|
|
332
|
+
|
|
333
|
+
---------------- SERVER ERROR ----------------
|
|
334
|
+
${serverError}
|
|
335
|
+
|
|
336
|
+
Please return a NEW JSON payload that corrects the issue.
|
|
337
|
+
Respond with JSON only – no comments or explanations.
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
let result;
|
|
341
|
+
try {
|
|
342
|
+
result = await this.chat.sendMessage({ message: prompt });
|
|
343
|
+
} catch (err) {
|
|
344
|
+
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const text = result.text ?? result.response ?? '';
|
|
349
|
+
return typeof text === 'object' ? text : JSON.parse(text);
|
|
350
|
+
} catch (parseErr) {
|
|
351
|
+
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resets the current chat session, clearing all history and examples
|
|
357
|
+
* @this {ExportedAPI}
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async function resetChat() {
|
|
361
|
+
if (this.chat) {
|
|
362
|
+
log.debug("Resetting Gemini chat session...");
|
|
363
|
+
this.chat = await this.genAIClient.chats.create({
|
|
364
|
+
model: this.modelName,
|
|
365
|
+
// @ts-ignore
|
|
366
|
+
config: this.chatConfig,
|
|
367
|
+
history: [],
|
|
368
|
+
});
|
|
369
|
+
log.debug("Chat session reset.");
|
|
370
|
+
} else {
|
|
371
|
+
log.warn("Cannot reset chat session: chat not yet initialized.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Retrieves the current conversation history for debugging or inspection
|
|
377
|
+
* @returns {Array<Object>} - An array of message objects in the conversation.
|
|
378
|
+
*/
|
|
379
|
+
function getChatHistory() {
|
|
380
|
+
if (!this.chat) {
|
|
381
|
+
log.warn("Chat session not initialized. No history available.");
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
return this.chat.getHistory();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
389
|
+
log.info("RUNNING AI Transformer as standalone script...");
|
|
390
|
+
(
|
|
391
|
+
async () => {
|
|
392
|
+
try {
|
|
393
|
+
log.info("Initializing AI Transformer...");
|
|
394
|
+
const transformer = new AITransformer({
|
|
395
|
+
modelName: 'gemini-2.0-flash',
|
|
396
|
+
sourceKey: 'INPUT', // Custom source key
|
|
397
|
+
targetKey: 'OUTPUT', // Custom target key
|
|
398
|
+
contextKey: 'CONTEXT', // Custom context key
|
|
399
|
+
maxRetries: 2,
|
|
400
|
+
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const examples = [
|
|
404
|
+
{
|
|
405
|
+
CONTEXT: "Generate professional profiles with emoji representations",
|
|
406
|
+
INPUT: { "name": "Alice" },
|
|
407
|
+
OUTPUT: { "name": "Alice", "profession": "data scientist", "life_as_told_by_emoji": ["🔬", "💡", "📊", "🧠", "🌟"] }
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
INPUT: { "name": "Bob" },
|
|
411
|
+
OUTPUT: { "name": "Bob", "profession": "product manager", "life_as_told_by_emoji": ["📋", "🤝", "🚀", "💬", "🎯"] }
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
INPUT: { "name": "Eve" },
|
|
415
|
+
OUTPUT: { "name": "Even", "profession": "security analyst", "life_as_told_by_emoji": ["🕵️♀️", "🔒", "💻", "👀", "⚡️"] }
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
await transformer.init();
|
|
420
|
+
await transformer.seed(examples);
|
|
421
|
+
log.info("AI Transformer initialized and seeded with examples.");
|
|
422
|
+
|
|
423
|
+
// Test normal transformation
|
|
424
|
+
const normalResponse = await transformer.message({ "name": "AK" });
|
|
425
|
+
log.info("Normal Payload Transformed", normalResponse);
|
|
426
|
+
|
|
427
|
+
// Test transformation with validation
|
|
428
|
+
const mockValidator = async (payload) => {
|
|
429
|
+
// Simulate validation logic
|
|
430
|
+
if (!payload.profession || !payload.life_as_told_by_emoji) {
|
|
431
|
+
throw new Error("Missing required fields: profession or life_as_told_by_emoji");
|
|
432
|
+
}
|
|
433
|
+
if (!Array.isArray(payload.life_as_told_by_emoji)) {
|
|
434
|
+
throw new Error("life_as_told_by_emoji must be an array");
|
|
435
|
+
}
|
|
436
|
+
return payload; // Return the payload if validation passes
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const validatedResponse = await transformer.transformWithValidation(
|
|
440
|
+
{ "name": "Lynn" },
|
|
441
|
+
mockValidator
|
|
442
|
+
);
|
|
443
|
+
log.info("Validated Payload Transformed", validatedResponse);
|
|
444
|
+
|
|
445
|
+
if (NODE_ENV === 'dev') debugger;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
log.error("Error in AI Transformer script:", error);
|
|
448
|
+
if (NODE_ENV === 'dev') debugger;
|
|
449
|
+
}
|
|
450
|
+
})();
|
|
451
|
+
}
|
package/logger.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
// Optional: Configure based on environment (dev vs prod)
|
|
4
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
5
|
+
|
|
6
|
+
const logger = pino({
|
|
7
|
+
level: process.env.LOG_LEVEL || 'info', // Supports 'fatal', 'error', 'warn', 'info', 'debug', 'trace'
|
|
8
|
+
transport: isDev
|
|
9
|
+
? {
|
|
10
|
+
target: 'pino-pretty', // Prettified output for local dev
|
|
11
|
+
options: { colorize: true, translateTime: true }
|
|
12
|
+
}
|
|
13
|
+
: undefined // In prod/cloud, keep as JSON for cloud logging
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default logger;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ak-gemini",
|
|
3
|
+
"author": "ak@mixpanel.com",
|
|
4
|
+
"description": "AK's Generative AI Helper for doing... transforms",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"index.cjs",
|
|
10
|
+
"types.ts",
|
|
11
|
+
"logger.js"
|
|
12
|
+
],
|
|
13
|
+
"types": "types.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./index.js",
|
|
17
|
+
"require": "./index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ak--47/ak-gemini"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/ak--47/ak-gemini/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/ak--47/ak-gemini#readme",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepublishOnly": "npm run build:cjs",
|
|
30
|
+
"publish": "npm publish --access public",
|
|
31
|
+
"release": "npm version patch && npm publish --access public",
|
|
32
|
+
"local": "./scripts/local.sh",
|
|
33
|
+
"fire": "./scripts/fire.sh",
|
|
34
|
+
"deploy": "./scripts/deploy.sh",
|
|
35
|
+
"perms": "chmod +x ./scripts/*.sh",
|
|
36
|
+
"update-deps": "npx npm-check-updates -u && npm install",
|
|
37
|
+
"prune": "rm -rf tmp/*",
|
|
38
|
+
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
39
|
+
"build:cjs": "esbuild index.js --bundle --platform=node --format=cjs --outfile=index.cjs"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"gemini",
|
|
44
|
+
"ai wrapper",
|
|
45
|
+
"json transform"
|
|
46
|
+
],
|
|
47
|
+
"license": "ISC",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@google-cloud/functions-framework": "^4.0.0",
|
|
50
|
+
"@google/genai": "^1.3.0",
|
|
51
|
+
"ak-tools": "^1.0.64",
|
|
52
|
+
"dotenv": "^16.5.0",
|
|
53
|
+
"pino": "^9.7.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/jest": "^29.5.14",
|
|
57
|
+
"esbuild": "^0.25.5",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"nodemon": "^3.1.10",
|
|
60
|
+
"pino-pretty": "^13.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { GoogleGenAI } from '@google/genai';
|
|
2
|
+
|
|
3
|
+
export interface SafetySetting {
|
|
4
|
+
category: string; // The harm category
|
|
5
|
+
threshold: string; // The blocking threshold
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChatConfig {
|
|
9
|
+
responseMimeType?: string; // MIME type for responses
|
|
10
|
+
temperature?: number; // Controls randomness (0.0 to 1.0)
|
|
11
|
+
topP?: number; // Controls diversity via nucleus sampling
|
|
12
|
+
topK?: number; // Controls diversity by limiting top-k tokens
|
|
13
|
+
systemInstruction?: string; // System instruction for the model
|
|
14
|
+
safetySettings?: SafetySetting[]; // Safety settings array
|
|
15
|
+
responseSchema?: Object; // Schema for validating model responses
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AITransformerContext {
|
|
19
|
+
modelName?: string;
|
|
20
|
+
systemInstructions?: string;
|
|
21
|
+
chatConfig?: ChatConfig;
|
|
22
|
+
genAI?: any;
|
|
23
|
+
chat?: any;
|
|
24
|
+
examplesFile?: string | null;
|
|
25
|
+
exampleData?: TransformationExample[] | null;
|
|
26
|
+
promptKey?: string;
|
|
27
|
+
answerKey?: string;
|
|
28
|
+
contextKey?: string;
|
|
29
|
+
maxRetries?: number;
|
|
30
|
+
retryDelay?: number;
|
|
31
|
+
init: () => Promise<void>; // Initialization function
|
|
32
|
+
seed: () => Promise<void>; // Function to seed the transformer with examples
|
|
33
|
+
message: (payload: Record<string, unknown>) => Promise<Record<string, unknown>>; // Function to send messages to the model
|
|
34
|
+
genAIClient?: GoogleGenAI; // Google GenAI client instance
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TransformationExample {
|
|
39
|
+
CONTEXT?: Record<string, unknown>; // optional context for the transformation
|
|
40
|
+
PROMPT?: Record<string, unknown>; // what the user provides as input
|
|
41
|
+
ANSWER?: Record<string, unknown>; // what the model should return as output
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ExampleFileContent {
|
|
45
|
+
examples: TransformationExample[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AITransformerOptions {
|
|
49
|
+
modelName?: string; // The Gemini model to use
|
|
50
|
+
systemInstructions?: string; // Custom system instructions for the model
|
|
51
|
+
chatConfig?: ChatConfig; // Configuration object for the chat session
|
|
52
|
+
examplesFile?: string; // Path to JSON file containing transformation examples
|
|
53
|
+
exampleData?: TransformationExample[]; // Inline examples to seed the transformer
|
|
54
|
+
sourceKey?: string; // Key name for source data in examples
|
|
55
|
+
targetKey?: string; // Key name for target data in examples
|
|
56
|
+
contextKey?: string; // Key name for context data in examples
|
|
57
|
+
maxRetries?: number; // Maximum retry attempts for auto-retry functionality
|
|
58
|
+
retryDelay?: number; // Initial retry delay in milliseconds
|
|
59
|
+
// ? https://ai.google.dev/gemini-api/docs/structured-output
|
|
60
|
+
responseSchema?: Object; // Schema for validating model responses
|
|
61
|
+
apiKey?: string; // API key for Google GenAI
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Async validator function type
|
|
65
|
+
export type AsyncValidatorFunction = (payload: Record<string, unknown>) => Promise<Record<string, unknown>>;
|