ak-gemini 1.1.13 → 2.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 +259 -294
- package/base.js +485 -0
- package/chat.js +87 -0
- package/code-agent.js +563 -0
- package/index.cjs +1596 -789
- package/index.js +38 -1500
- package/json-helpers.js +352 -0
- package/message.js +170 -0
- package/package.json +23 -14
- package/tool-agent.js +311 -0
- package/transformer.js +502 -0
- package/types.d.ts +376 -189
package/index.js
CHANGED
|
@@ -1,1503 +1,41 @@
|
|
|
1
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, LOG_LEVEL = "" } = process.env;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//deps
|
|
26
|
-
import { GoogleGenAI, HarmCategory, HarmBlockThreshold, ThinkingLevel } from '@google/genai';
|
|
27
|
-
import u from 'ak-tools';
|
|
28
|
-
import path from 'path';
|
|
29
|
-
import log from './logger.js';
|
|
30
|
-
export { log };
|
|
31
|
-
export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// defaults
|
|
36
|
-
const DEFAULT_SAFETY_SETTINGS = [
|
|
37
|
-
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
|
|
38
|
-
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const DEFAULT_SYSTEM_INSTRUCTIONS = `
|
|
42
|
-
You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
|
|
43
|
-
|
|
44
|
-
You will be provided with example transformations (Source JSON -> Target JSON).
|
|
45
|
-
|
|
46
|
-
Learn the mapping rules from these examples.
|
|
47
|
-
|
|
48
|
-
When presented with new Source JSON, apply the learned transformation rules to produce a new Target JSON payload.
|
|
49
|
-
|
|
50
|
-
Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
|
|
51
|
-
|
|
52
|
-
Do not include any additional text, explanations, or formatting before or after the JSON object.
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
const DEFAULT_THINKING_CONFIG = {
|
|
56
|
-
thinkingBudget: 0
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 50_000; // Default ceiling for output tokens
|
|
60
|
-
|
|
61
|
-
// Models that support thinking features (as of Dec 2024)
|
|
62
|
-
// Using regex patterns for more precise matching
|
|
63
|
-
const THINKING_SUPPORTED_MODELS = [
|
|
64
|
-
/^gemini-3-flash(-preview)?$/,
|
|
65
|
-
/^gemini-3-pro(-preview|-image-preview)?$/,
|
|
66
|
-
/^gemini-2\.5-pro/,
|
|
67
|
-
/^gemini-2\.5-flash(-preview)?$/,
|
|
68
|
-
/^gemini-2\.5-flash-lite(-preview)?$/,
|
|
69
|
-
/^gemini-2\.0-flash$/ // Experimental support, exact match only
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const DEFAULT_CHAT_CONFIG = {
|
|
73
|
-
responseMimeType: 'application/json',
|
|
74
|
-
temperature: 0.2,
|
|
75
|
-
topP: 0.95,
|
|
76
|
-
topK: 64,
|
|
77
|
-
systemInstruction: DEFAULT_SYSTEM_INSTRUCTIONS,
|
|
78
|
-
safetySettings: DEFAULT_SAFETY_SETTINGS
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @typedef {import('./types').AITransformer} AITransformerUtility
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* main export class for AI Transformer
|
|
89
|
-
* @class AITransformer
|
|
90
|
-
* @type {AITransformerUtility}
|
|
91
|
-
* @description A class that provides methods to initialize, seed, transform, and manage AI-based transformations using Google Gemini API.
|
|
92
|
-
* @implements {ExportedAPI}
|
|
93
|
-
*/
|
|
94
|
-
class AITransformer {
|
|
95
|
-
/**
|
|
96
|
-
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
97
|
-
*
|
|
98
|
-
*/
|
|
99
|
-
constructor(options = {}) {
|
|
100
|
-
this.modelName = "";
|
|
101
|
-
this.promptKey = "";
|
|
102
|
-
this.answerKey = "";
|
|
103
|
-
this.contextKey = "";
|
|
104
|
-
this.explanationKey = "";
|
|
105
|
-
this.systemInstructionKey = "";
|
|
106
|
-
this.maxRetries = 3;
|
|
107
|
-
this.retryDelay = 1000;
|
|
108
|
-
// this.systemInstructions = "";
|
|
109
|
-
this.chatConfig = {};
|
|
110
|
-
this.apiKey = GEMINI_API_KEY;
|
|
111
|
-
this.onlyJSON = true; // always return JSON
|
|
112
|
-
this.asyncValidator = null; // for transformWithValidation
|
|
113
|
-
this.logLevel = 'info'; // default log level
|
|
114
|
-
this.lastResponseMetadata = null; // stores metadata from last API response
|
|
115
|
-
this.exampleCount = 0; // tracks number of example history items from seed()
|
|
116
|
-
// Cumulative usage tracking across retry attempts
|
|
117
|
-
this._cumulativeUsage = {
|
|
118
|
-
promptTokens: 0,
|
|
119
|
-
responseTokens: 0,
|
|
120
|
-
totalTokens: 0,
|
|
121
|
-
attempts: 0
|
|
122
|
-
};
|
|
123
|
-
AITransformFactory.call(this, options);
|
|
124
|
-
|
|
125
|
-
//external API
|
|
126
|
-
this.init = initChat.bind(this);
|
|
127
|
-
this.seed = seedWithExamples.bind(this);
|
|
128
|
-
|
|
129
|
-
// Internal "raw" message sender
|
|
130
|
-
this.rawMessage = rawMessage.bind(this);
|
|
131
|
-
|
|
132
|
-
// The public `.message()` method uses the GLOBAL validator
|
|
133
|
-
this.message = (payload, opts = {}, validatorFn = null) => {
|
|
134
|
-
|
|
135
|
-
return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
this.rebuild = rebuildPayload.bind(this);
|
|
139
|
-
this.reset = resetChat.bind(this);
|
|
140
|
-
this.getHistory = getChatHistory.bind(this);
|
|
141
|
-
this.messageAndValidate = prepareAndValidateMessage.bind(this);
|
|
142
|
-
this.transformWithValidation = prepareAndValidateMessage.bind(this);
|
|
143
|
-
this.estimate = estimateInputTokens.bind(this);
|
|
144
|
-
this.updateSystemInstructions = updateSystemInstructions.bind(this);
|
|
145
|
-
this.estimateCost = estimateCost.bind(this);
|
|
146
|
-
this.clearConversation = clearConversation.bind(this);
|
|
147
|
-
this.getLastUsage = getLastUsage.bind(this);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export default AITransformer;
|
|
152
|
-
export { attemptJSONRecovery }; // Export for testing
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* factory function to create an AI Transformer instance
|
|
156
|
-
* @param {AITransformerOptions} [options={}] - Configuration options for the transformer
|
|
157
|
-
* @returns {void} - An instance of AITransformer with initialized properties and methods
|
|
158
|
-
*/
|
|
159
|
-
function AITransformFactory(options = {}) {
|
|
160
|
-
// ? https://ai.google.dev/gemini-api/docs/models
|
|
161
|
-
this.modelName = options.modelName || 'gemini-2.5-flash';
|
|
162
|
-
|
|
163
|
-
// Only use default if systemInstructions was not provided at all
|
|
164
|
-
if (options.systemInstructions === undefined) {
|
|
165
|
-
this.systemInstructions = DEFAULT_SYSTEM_INSTRUCTIONS;
|
|
166
|
-
} else {
|
|
167
|
-
// Use the provided value (could be null, false, or a custom string)
|
|
168
|
-
this.systemInstructions = options.systemInstructions;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Configure log level - priority: options.logLevel > LOG_LEVEL env > NODE_ENV based defaults > 'info'
|
|
172
|
-
if (options.logLevel) {
|
|
173
|
-
this.logLevel = options.logLevel;
|
|
174
|
-
if (this.logLevel === 'none') {
|
|
175
|
-
// Set to silent to disable all logging
|
|
176
|
-
log.level = 'silent';
|
|
177
|
-
} else {
|
|
178
|
-
// Set the log level as specified
|
|
179
|
-
log.level = this.logLevel;
|
|
180
|
-
}
|
|
181
|
-
} else if (LOG_LEVEL) {
|
|
182
|
-
// Use environment variable if no option specified
|
|
183
|
-
this.logLevel = LOG_LEVEL;
|
|
184
|
-
log.level = LOG_LEVEL;
|
|
185
|
-
} else if (NODE_ENV === 'dev') {
|
|
186
|
-
this.logLevel = 'debug';
|
|
187
|
-
log.level = 'debug';
|
|
188
|
-
} else if (NODE_ENV === 'test') {
|
|
189
|
-
this.logLevel = 'warn';
|
|
190
|
-
log.level = 'warn';
|
|
191
|
-
} else if (NODE_ENV.startsWith('prod')) {
|
|
192
|
-
this.logLevel = 'error';
|
|
193
|
-
log.level = 'error';
|
|
194
|
-
} else {
|
|
195
|
-
// Default to info
|
|
196
|
-
this.logLevel = 'info';
|
|
197
|
-
log.level = 'info';
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Vertex AI configuration
|
|
201
|
-
this.vertexai = options.vertexai || false;
|
|
202
|
-
this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
|
|
203
|
-
this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || undefined;
|
|
204
|
-
this.googleAuthOptions = options.googleAuthOptions || null;
|
|
205
|
-
|
|
206
|
-
// API Key (for Gemini API, not Vertex AI)
|
|
207
|
-
this.apiKey = options.apiKey !== undefined && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
|
|
208
|
-
|
|
209
|
-
// Validate authentication - need either API key (for Gemini API) or Vertex AI config
|
|
210
|
-
if (!this.vertexai && !this.apiKey) {
|
|
211
|
-
throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var. For Vertex AI, set vertexai: true with project and location.");
|
|
212
|
-
}
|
|
213
|
-
if (this.vertexai && !this.project) {
|
|
214
|
-
throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Build chat config, making sure systemInstruction uses the custom instructions
|
|
218
|
-
this.chatConfig = {
|
|
219
|
-
...DEFAULT_CHAT_CONFIG,
|
|
220
|
-
...options.chatConfig
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// Handle systemInstructions: use custom if provided, otherwise keep default from DEFAULT_CHAT_CONFIG
|
|
224
|
-
// If explicitly set to null/false, remove it entirely
|
|
225
|
-
if (this.systemInstructions) {
|
|
226
|
-
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
227
|
-
} else if (options.systemInstructions !== undefined) {
|
|
228
|
-
// Explicitly set to null/false/empty - remove system instruction
|
|
229
|
-
delete this.chatConfig.systemInstruction;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Handle maxOutputTokens with explicit null check
|
|
233
|
-
// Priority: options.maxOutputTokens > options.chatConfig.maxOutputTokens > DEFAULT
|
|
234
|
-
// Setting to null explicitly removes the limit
|
|
235
|
-
if (options.maxOutputTokens !== undefined) {
|
|
236
|
-
if (options.maxOutputTokens === null) {
|
|
237
|
-
delete this.chatConfig.maxOutputTokens;
|
|
238
|
-
} else {
|
|
239
|
-
this.chatConfig.maxOutputTokens = options.maxOutputTokens;
|
|
240
|
-
}
|
|
241
|
-
} else if (options.chatConfig?.maxOutputTokens !== undefined) {
|
|
242
|
-
if (options.chatConfig.maxOutputTokens === null) {
|
|
243
|
-
delete this.chatConfig.maxOutputTokens;
|
|
244
|
-
} else {
|
|
245
|
-
this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Only add thinkingConfig if the model supports it
|
|
252
|
-
const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(pattern =>
|
|
253
|
-
pattern.test(this.modelName)
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
// Handle thinkingConfig - null explicitly removes it, undefined means not specified
|
|
257
|
-
if (options.thinkingConfig !== undefined) {
|
|
258
|
-
if (options.thinkingConfig === null) {
|
|
259
|
-
// Explicitly remove thinkingConfig if set to null
|
|
260
|
-
delete this.chatConfig.thinkingConfig;
|
|
261
|
-
if (log.level !== 'silent') {
|
|
262
|
-
log.debug(`thinkingConfig set to null - removed from configuration`);
|
|
263
|
-
}
|
|
264
|
-
} else if (modelSupportsThinking) {
|
|
265
|
-
// Handle thinkingConfig - merge with defaults
|
|
266
|
-
const thinkingConfig = {
|
|
267
|
-
...DEFAULT_THINKING_CONFIG,
|
|
268
|
-
...options.thinkingConfig
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Gemini API does not allow both thinkingBudget and thinkingLevel together.
|
|
272
|
-
// If user specified thinkingLevel, remove thinkingBudget (user preference wins)
|
|
273
|
-
if (options.thinkingConfig?.thinkingLevel !== undefined) {
|
|
274
|
-
delete thinkingConfig.thinkingBudget;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
this.chatConfig.thinkingConfig = thinkingConfig;
|
|
278
|
-
|
|
279
|
-
if (log.level !== 'silent') {
|
|
280
|
-
log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
if (log.level !== 'silent') {
|
|
284
|
-
log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// response schema is optional, but if provided, it should be a valid JSON schema
|
|
290
|
-
if (options.responseSchema) {
|
|
291
|
-
this.chatConfig.responseSchema = options.responseSchema;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// examples file is optional, but if provided, it should contain valid PROMPT and ANSWER keys
|
|
295
|
-
this.examplesFile = options.examplesFile || null;
|
|
296
|
-
this.exampleData = options.exampleData || null; // can be used instead of examplesFile
|
|
297
|
-
|
|
298
|
-
// Use configurable keys with fallbacks
|
|
299
|
-
this.promptKey = options.promptKey || options.sourceKey || 'PROMPT';
|
|
300
|
-
this.answerKey = options.answerKey || options.targetKey || 'ANSWER';
|
|
301
|
-
this.contextKey = options.contextKey || 'CONTEXT'; // Optional key for context
|
|
302
|
-
this.explanationKey = options.explanationKey || 'EXPLANATION'; // Optional key for explanations
|
|
303
|
-
this.systemInstructionsKey = options.systemInstructionsKey || 'SYSTEM'; // Optional key for system instructions
|
|
304
|
-
|
|
305
|
-
// Retry configuration
|
|
306
|
-
this.maxRetries = options.maxRetries || 3;
|
|
307
|
-
this.retryDelay = options.retryDelay || 1000;
|
|
308
|
-
|
|
309
|
-
//allow async validation function
|
|
310
|
-
this.asyncValidator = options.asyncValidator || null; // Function to validate transformed payloads
|
|
311
|
-
|
|
312
|
-
//are we forcing json responses only?
|
|
313
|
-
this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
|
|
314
|
-
|
|
315
|
-
// Grounding configuration (disabled by default to avoid costs)
|
|
316
|
-
this.enableGrounding = options.enableGrounding || false;
|
|
317
|
-
this.groundingConfig = options.groundingConfig || {};
|
|
318
|
-
|
|
319
|
-
// Billing labels for cost segmentation (Vertex AI only)
|
|
320
|
-
this.labels = options.labels || {};
|
|
321
|
-
if (Object.keys(this.labels).length > 0 && log.level !== 'silent') {
|
|
322
|
-
if (!this.vertexai) {
|
|
323
|
-
log.warn(`Billing labels are only supported with Vertex AI. Labels will be ignored.`);
|
|
324
|
-
} else {
|
|
325
|
-
log.debug(`Billing labels configured: ${JSON.stringify(this.labels)}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (this.promptKey === this.answerKey) {
|
|
330
|
-
throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (log.level !== 'silent') {
|
|
334
|
-
log.debug(`Creating AI Transformer with model: ${this.modelName}`);
|
|
335
|
-
log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
|
|
336
|
-
log.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
|
|
337
|
-
// Log authentication method
|
|
338
|
-
if (this.vertexai) {
|
|
339
|
-
log.debug(`Using Vertex AI - Project: ${this.project}, Location: ${this.location || 'global (default)'}`);
|
|
340
|
-
if (this.googleAuthOptions?.keyFilename) {
|
|
341
|
-
log.debug(`Auth: Service account key file: ${this.googleAuthOptions.keyFilename}`);
|
|
342
|
-
} else if (this.googleAuthOptions?.credentials) {
|
|
343
|
-
log.debug(`Auth: Inline credentials provided`);
|
|
344
|
-
} else {
|
|
345
|
-
log.debug(`Auth: Application Default Credentials (ADC)`);
|
|
346
|
-
}
|
|
347
|
-
} else {
|
|
348
|
-
log.debug(`Using Gemini API with key: ${this.apiKey.substring(0, 10)}...`);
|
|
349
|
-
}
|
|
350
|
-
log.debug(`Grounding ${this.enableGrounding ? 'ENABLED' : 'DISABLED'} (costs $35/1k queries)`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Initialize Google GenAI client with appropriate configuration
|
|
354
|
-
const clientOptions = this.vertexai
|
|
355
|
-
? {
|
|
356
|
-
vertexai: true,
|
|
357
|
-
project: this.project,
|
|
358
|
-
...(this.location && { location: this.location }),
|
|
359
|
-
...(this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions })
|
|
360
|
-
}
|
|
361
|
-
: { apiKey: this.apiKey };
|
|
362
|
-
|
|
363
|
-
const ai = new GoogleGenAI(clientOptions);
|
|
364
|
-
this.genAIClient = ai;
|
|
365
|
-
this.chat = null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Initializes the chat session with the specified model and configurations.
|
|
370
|
-
* @param {boolean} [force=false] - If true, forces reinitialization of the chat session.
|
|
371
|
-
* @this {ExportedAPI}
|
|
372
|
-
* @returns {Promise<void>}
|
|
373
|
-
*/
|
|
374
|
-
async function initChat(force = false) {
|
|
375
|
-
if (this.chat && !force) return;
|
|
376
|
-
|
|
377
|
-
log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
|
|
378
|
-
|
|
379
|
-
// Add grounding tools if enabled
|
|
380
|
-
const chatOptions = {
|
|
381
|
-
model: this.modelName,
|
|
382
|
-
// @ts-ignore
|
|
383
|
-
config: {
|
|
384
|
-
...this.chatConfig,
|
|
385
|
-
...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
|
|
386
|
-
},
|
|
387
|
-
history: [],
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// Only add tools if grounding is explicitly enabled
|
|
391
|
-
if (this.enableGrounding) {
|
|
392
|
-
chatOptions.config.tools = [{
|
|
393
|
-
googleSearch: this.groundingConfig
|
|
394
|
-
}];
|
|
395
|
-
log.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
await this.genAIClient.models.list();
|
|
402
|
-
log.debug("Gemini API connection successful.");
|
|
403
|
-
} catch (e) {
|
|
404
|
-
throw new Error(`Gemini chat initialization failed: ${e.message}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
log.debug("Gemini chat session initialized.");
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Seeds the chat session with example transformations.
|
|
414
|
-
* @this {ExportedAPI}
|
|
415
|
-
* @param {TransformationExample[]} [examples] - An array of transformation examples.
|
|
416
|
-
* @this {ExportedAPI}
|
|
417
|
-
* @returns {Promise<void>}
|
|
418
|
-
*/
|
|
419
|
-
async function seedWithExamples(examples) {
|
|
420
|
-
await this.init();
|
|
421
|
-
|
|
422
|
-
if (!examples || !Array.isArray(examples) || examples.length === 0) {
|
|
423
|
-
if (this.examplesFile) {
|
|
424
|
-
log.debug(`No examples provided, loading from file: ${this.examplesFile}`);
|
|
425
|
-
try {
|
|
426
|
-
// @ts-ignore
|
|
427
|
-
examples = await u.load(path.resolve(this.examplesFile), true);
|
|
428
|
-
}
|
|
429
|
-
catch (err) {
|
|
430
|
-
throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
else if (this.exampleData) {
|
|
435
|
-
log.debug(`Using example data provided in options.`);
|
|
436
|
-
if (Array.isArray(this.exampleData)) {
|
|
437
|
-
examples = this.exampleData;
|
|
438
|
-
} else {
|
|
439
|
-
throw new Error(`Invalid example data provided. Expected an array of examples.`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
else {
|
|
444
|
-
log.debug("No examples provided and no examples file specified. Skipping seeding.");
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const instructionExample = examples.find(ex => ex[this.systemInstructionsKey]);
|
|
450
|
-
if (instructionExample) {
|
|
451
|
-
log.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
|
|
452
|
-
this.systemInstructions = instructionExample[this.systemInstructionsKey];
|
|
453
|
-
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
454
|
-
await this.init(true); // Reinitialize chat with new system instructions
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
log.debug(`Seeding chat with ${examples.length} transformation examples...`);
|
|
458
|
-
const historyToAdd = [];
|
|
459
|
-
|
|
460
|
-
for (const example of examples) {
|
|
461
|
-
// Use the configurable keys from constructor
|
|
462
|
-
const contextValue = example[this.contextKey] || "";
|
|
463
|
-
const promptValue = example[this.promptKey] || "";
|
|
464
|
-
const answerValue = example[this.answerKey] || "";
|
|
465
|
-
const explanationValue = example[this.explanationKey] || "";
|
|
466
|
-
let userText = "";
|
|
467
|
-
let modelResponse = {};
|
|
468
|
-
|
|
469
|
-
// Add context as user message with special formatting to make it part of the example flow
|
|
470
|
-
if (contextValue) {
|
|
471
|
-
let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
|
|
472
|
-
// Prefix context to make it clear it's contextual information
|
|
473
|
-
userText += `CONTEXT:\n${contextText}\n\n`;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (promptValue) {
|
|
477
|
-
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
478
|
-
userText += promptText;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (answerValue) modelResponse.data = answerValue;
|
|
482
|
-
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
483
|
-
const modelText = JSON.stringify(modelResponse, null, 2);
|
|
484
|
-
|
|
485
|
-
if (userText.trim().length && modelText.trim().length > 0) {
|
|
486
|
-
historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
|
|
487
|
-
historyToAdd.push({ role: 'model', parts: [{ text: modelText.trim() }] });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const currentHistory = this?.chat?.getHistory() || [];
|
|
494
|
-
log.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
|
|
495
|
-
this.chat = await this.genAIClient.chats.create({
|
|
496
|
-
model: this.modelName,
|
|
497
|
-
// @ts-ignore
|
|
498
|
-
config: {
|
|
499
|
-
...this.chatConfig,
|
|
500
|
-
...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
|
|
501
|
-
},
|
|
502
|
-
history: [...currentHistory, ...historyToAdd],
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Track example count for clearConversation() and stateless messages
|
|
506
|
-
this.exampleCount = currentHistory.length + historyToAdd.length;
|
|
507
|
-
|
|
508
|
-
const newHistory = this.chat.getHistory();
|
|
509
|
-
log.debug(`Created new chat session with ${newHistory.length} examples.`);
|
|
510
|
-
return newHistory;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Transforms a source JSON payload into a target JSON payload
|
|
515
|
-
* @param {Object} sourcePayload - The source payload (as a JavaScript object).
|
|
516
|
-
* @returns {Promise<Object>} - The transformed target payload (as a JavaScript object).
|
|
517
|
-
* @throws {Error} If the transformation fails or returns invalid JSON.
|
|
518
|
-
*/
|
|
519
|
-
/**
|
|
520
|
-
* (Internal) Sends a single prompt to the model and parses the response.
|
|
521
|
-
* No validation or retry logic.
|
|
522
|
-
* @this {ExportedAPI}
|
|
523
|
-
* @param {Object|string} sourcePayload - The source payload.
|
|
524
|
-
* @param {Object} [messageOptions] - Optional per-message options (e.g., labels).
|
|
525
|
-
* @returns {Promise<Object>} - The transformed payload.
|
|
526
|
-
*/
|
|
527
|
-
async function rawMessage(sourcePayload, messageOptions = {}) {
|
|
528
|
-
if (!this.chat) {
|
|
529
|
-
throw new Error("Chat session not initialized.");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const actualPayload = typeof sourcePayload === 'string'
|
|
533
|
-
? sourcePayload
|
|
534
|
-
: JSON.stringify(sourcePayload, null, 2);
|
|
535
|
-
|
|
536
|
-
// Merge instance labels with per-message labels (per-message takes precedence)
|
|
537
|
-
// Labels only supported with Vertex AI
|
|
538
|
-
const mergedLabels = { ...this.labels, ...(messageOptions.labels || {}) };
|
|
539
|
-
const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
|
|
540
|
-
|
|
541
|
-
try {
|
|
542
|
-
const sendParams = { message: actualPayload };
|
|
543
|
-
|
|
544
|
-
// Add config with labels if we have any (Vertex AI only)
|
|
545
|
-
if (hasLabels) {
|
|
546
|
-
sendParams.config = { labels: mergedLabels };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const result = await this.chat.sendMessage(sendParams);
|
|
550
|
-
|
|
551
|
-
// Capture and log response metadata for model verification and debugging
|
|
552
|
-
this.lastResponseMetadata = {
|
|
553
|
-
modelVersion: result.modelVersion || null,
|
|
554
|
-
requestedModel: this.modelName,
|
|
555
|
-
promptTokens: result.usageMetadata?.promptTokenCount || 0,
|
|
556
|
-
responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
|
|
557
|
-
totalTokens: result.usageMetadata?.totalTokenCount || 0,
|
|
558
|
-
timestamp: Date.now()
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
if (result.usageMetadata && log.level !== 'silent') {
|
|
562
|
-
log.debug(`API response metadata:`, {
|
|
563
|
-
modelVersion: result.modelVersion || 'not-provided',
|
|
564
|
-
requestedModel: this.modelName,
|
|
565
|
-
promptTokens: result.usageMetadata.promptTokenCount,
|
|
566
|
-
responseTokens: result.usageMetadata.candidatesTokenCount,
|
|
567
|
-
totalTokens: result.usageMetadata.totalTokenCount
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const modelResponse = result.text;
|
|
572
|
-
const extractedJSON = extractJSON(modelResponse); // Assuming extractJSON is defined
|
|
573
|
-
|
|
574
|
-
// Unwrap the 'data' property if it exists
|
|
575
|
-
if (extractedJSON?.data) {
|
|
576
|
-
return extractedJSON.data;
|
|
577
|
-
}
|
|
578
|
-
return extractedJSON;
|
|
579
|
-
|
|
580
|
-
} catch (error) {
|
|
581
|
-
if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
|
|
582
|
-
throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
|
|
583
|
-
}
|
|
584
|
-
// For other API errors, just re-throw
|
|
585
|
-
throw new Error(`Transformation failed: ${error.message}`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* (Engine) Transforms a payload with validation and automatic retry logic.
|
|
591
|
-
* @this {ExportedAPI}
|
|
592
|
-
* @param {Object} sourcePayload - The source payload to transform.
|
|
593
|
-
* @param {Object} [options] - Options for the validation process.
|
|
594
|
-
* @param {AsyncValidatorFunction | null} validatorFn - The specific validator to use for this run.
|
|
595
|
-
* @returns {Promise<Object>} - The validated transformed payload.
|
|
596
|
-
*/
|
|
597
|
-
async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
598
|
-
if (!this.chat) {
|
|
599
|
-
throw new Error("Chat session not initialized. Please call init() first.");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Handle stateless messages separately - they don't add to chat history
|
|
603
|
-
if (options.stateless) {
|
|
604
|
-
return await statelessMessage.call(this, sourcePayload, options, validatorFn);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
608
|
-
const retryDelay = options.retryDelay ?? this.retryDelay;
|
|
609
|
-
|
|
610
|
-
// Check if grounding should be enabled for this specific message
|
|
611
|
-
const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
|
|
612
|
-
const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
|
|
613
|
-
|
|
614
|
-
// Reinitialize chat if grounding settings changed for this message
|
|
615
|
-
if (enableGroundingForMessage !== this.enableGrounding) {
|
|
616
|
-
const originalGrounding = this.enableGrounding;
|
|
617
|
-
const originalConfig = this.groundingConfig;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
// Temporarily change grounding settings
|
|
621
|
-
this.enableGrounding = enableGroundingForMessage;
|
|
622
|
-
this.groundingConfig = groundingConfigForMessage;
|
|
623
|
-
|
|
624
|
-
// Force reinit with new settings
|
|
625
|
-
await this.init(true);
|
|
626
|
-
|
|
627
|
-
// Log the change
|
|
628
|
-
if (enableGroundingForMessage) {
|
|
629
|
-
log.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
|
|
630
|
-
} else {
|
|
631
|
-
log.debug(`Search grounding DISABLED for this message`);
|
|
632
|
-
}
|
|
633
|
-
} catch (error) {
|
|
634
|
-
// Restore original settings on error
|
|
635
|
-
this.enableGrounding = originalGrounding;
|
|
636
|
-
this.groundingConfig = originalConfig;
|
|
637
|
-
throw error;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Schedule restoration after message completes
|
|
641
|
-
const restoreGrounding = async () => {
|
|
642
|
-
this.enableGrounding = originalGrounding;
|
|
643
|
-
this.groundingConfig = originalConfig;
|
|
644
|
-
await this.init(true);
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
// Store restoration function to call after message completes
|
|
648
|
-
options._restoreGrounding = restoreGrounding;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
let lastError = null;
|
|
652
|
-
let lastPayload = null; // Store the payload that caused the validation error
|
|
653
|
-
|
|
654
|
-
// Prepare the payload
|
|
655
|
-
if (sourcePayload && isJSON(sourcePayload)) {
|
|
656
|
-
lastPayload = JSON.stringify(sourcePayload, null, 2);
|
|
657
|
-
} else if (typeof sourcePayload === 'string') {
|
|
658
|
-
lastPayload = sourcePayload;
|
|
659
|
-
}
|
|
660
|
-
else if (typeof sourcePayload === 'boolean' || typeof sourcePayload === 'number') {
|
|
661
|
-
lastPayload = sourcePayload.toString();
|
|
662
|
-
}
|
|
663
|
-
else if (sourcePayload === null || sourcePayload === undefined) {
|
|
664
|
-
lastPayload = JSON.stringify({}); // Convert null/undefined to empty object
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
throw new Error("Invalid source payload. Must be a JSON object or string.");
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Extract per-message labels for passing to rawMessage
|
|
671
|
-
const messageOptions = {};
|
|
672
|
-
if (options.labels) {
|
|
673
|
-
messageOptions.labels = options.labels;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Reset cumulative usage tracking for this message call
|
|
677
|
-
this._cumulativeUsage = {
|
|
678
|
-
promptTokens: 0,
|
|
679
|
-
responseTokens: 0,
|
|
680
|
-
totalTokens: 0,
|
|
681
|
-
attempts: 0
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
685
|
-
try {
|
|
686
|
-
// Step 1: Get the transformed payload
|
|
687
|
-
const transformedPayload = (attempt === 0)
|
|
688
|
-
? await this.rawMessage(lastPayload, messageOptions) // Use the new raw method with per-message options
|
|
689
|
-
: await this.rebuild(lastPayload, lastError.message);
|
|
690
|
-
|
|
691
|
-
// Accumulate token usage from this attempt
|
|
692
|
-
if (this.lastResponseMetadata) {
|
|
693
|
-
this._cumulativeUsage.promptTokens += this.lastResponseMetadata.promptTokens || 0;
|
|
694
|
-
this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
|
|
695
|
-
this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
|
|
696
|
-
this._cumulativeUsage.attempts = attempt + 1;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
lastPayload = transformedPayload; // Always update lastPayload *before* validation
|
|
700
|
-
|
|
701
|
-
// Step 2: Validate if a validator is provided
|
|
702
|
-
if (validatorFn) {
|
|
703
|
-
await validatorFn(transformedPayload); // Validator throws on failure
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Step 3: Success!
|
|
707
|
-
log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
|
|
708
|
-
|
|
709
|
-
// Restore original grounding settings if they were changed
|
|
710
|
-
if (options._restoreGrounding) {
|
|
711
|
-
await options._restoreGrounding();
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return transformedPayload;
|
|
715
|
-
|
|
716
|
-
} catch (error) {
|
|
717
|
-
lastError = error;
|
|
718
|
-
log.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
|
|
719
|
-
|
|
720
|
-
if (attempt >= maxRetries) {
|
|
721
|
-
log.error(`All ${maxRetries + 1} attempts failed.`)
|
|
722
|
-
;
|
|
723
|
-
// Restore original grounding settings even on failure
|
|
724
|
-
if (options._restoreGrounding) {
|
|
725
|
-
await options._restoreGrounding();
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Wait before retrying
|
|
732
|
-
const delay = retryDelay * Math.pow(2, attempt);
|
|
733
|
-
await new Promise(res => setTimeout(res, delay));
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Rebuilds a payload based on server error feedback
|
|
740
|
-
* @this {ExportedAPI}
|
|
741
|
-
* @param {Object} lastPayload - The payload that failed validation
|
|
742
|
-
* @param {string} serverError - The error message from the server
|
|
743
|
-
* @returns {Promise<Object>} - A new corrected payload
|
|
744
|
-
* @throws {Error} If the rebuild process fails.
|
|
745
|
-
*/
|
|
746
|
-
async function rebuildPayload(lastPayload, serverError) {
|
|
747
|
-
await this.init(); // Ensure chat is initialized
|
|
748
|
-
const prompt = `
|
|
749
|
-
The previous JSON payload (below) failed validation.
|
|
750
|
-
The server's error message is quoted afterward.
|
|
751
|
-
|
|
752
|
-
---------------- BAD PAYLOAD ----------------
|
|
753
|
-
${JSON.stringify(lastPayload, null, 2)}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
---------------- SERVER ERROR ----------------
|
|
757
|
-
${serverError}
|
|
758
|
-
|
|
759
|
-
Please return a NEW JSON payload that corrects the issue.
|
|
760
|
-
Respond with JSON only – no comments or explanations.
|
|
761
|
-
`;
|
|
762
|
-
|
|
763
|
-
let result;
|
|
764
|
-
try {
|
|
765
|
-
result = await this.chat.sendMessage({ message: prompt });
|
|
766
|
-
|
|
767
|
-
// Capture and log response metadata for rebuild calls too
|
|
768
|
-
this.lastResponseMetadata = {
|
|
769
|
-
modelVersion: result.modelVersion || null,
|
|
770
|
-
requestedModel: this.modelName,
|
|
771
|
-
promptTokens: result.usageMetadata?.promptTokenCount || 0,
|
|
772
|
-
responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
|
|
773
|
-
totalTokens: result.usageMetadata?.totalTokenCount || 0,
|
|
774
|
-
timestamp: Date.now()
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
if (result.usageMetadata && log.level !== 'silent') {
|
|
778
|
-
log.debug(`Rebuild response metadata - tokens used:`, result.usageMetadata.totalTokenCount);
|
|
779
|
-
}
|
|
780
|
-
} catch (err) {
|
|
781
|
-
throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
try {
|
|
785
|
-
const text = result.text ?? result.response ?? '';
|
|
786
|
-
return typeof text === 'object' ? text : JSON.parse(text);
|
|
787
|
-
} catch (parseErr) {
|
|
788
|
-
throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Estimate INPUT tokens only for a payload before sending.
|
|
797
|
-
* This estimates the tokens that will be consumed by your prompt (input), NOT the response (output).
|
|
798
|
-
* Includes: system instructions + chat history (seeded examples) + your new message.
|
|
799
|
-
* Use this to preview input token costs and avoid exceeding context window limits.
|
|
2
|
+
* @fileoverview ak-gemini — Easy-to-use wrappers on @google/genai.
|
|
800
3
|
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
|
|
808
|
-
async function estimateInputTokens(nextPayload) {
|
|
809
|
-
// Compose the conversation contents, Gemini-style
|
|
810
|
-
const contents = [];
|
|
811
|
-
|
|
812
|
-
// (1) System instructions (if applicable)
|
|
813
|
-
if (this.systemInstructions) {
|
|
814
|
-
// Add as a 'system' part; adjust role if Gemini supports
|
|
815
|
-
contents.push({ parts: [{ text: this.systemInstructions }] });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// (2) All current chat history (seeded examples + real user/model turns)
|
|
819
|
-
if (this.chat && typeof this.chat.getHistory === "function") {
|
|
820
|
-
const history = this.chat.getHistory();
|
|
821
|
-
if (Array.isArray(history) && history.length > 0) {
|
|
822
|
-
contents.push(...history);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// (3) The next user message
|
|
827
|
-
const nextMessage = typeof nextPayload === "string"
|
|
828
|
-
? nextPayload
|
|
829
|
-
: JSON.stringify(nextPayload, null, 2);
|
|
830
|
-
|
|
831
|
-
contents.push({ parts: [{ text: nextMessage }] });
|
|
832
|
-
|
|
833
|
-
// Call Gemini's token estimator
|
|
834
|
-
const resp = await this.genAIClient.models.countTokens({
|
|
835
|
-
model: this.modelName,
|
|
836
|
-
contents,
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// Return with clear naming - this is INPUT tokens only
|
|
840
|
-
return { inputTokens: resp.totalTokens };
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Model pricing per million tokens (as of Dec 2025)
|
|
844
|
-
// https://ai.google.dev/gemini-api/docs/pricing
|
|
845
|
-
const MODEL_PRICING = {
|
|
846
|
-
'gemini-2.5-flash': { input: 0.15, output: 0.60 },
|
|
847
|
-
'gemini-2.5-flash-lite': { input: 0.02, output: 0.10 },
|
|
848
|
-
'gemini-2.5-pro': { input: 2.50, output: 10.00 },
|
|
849
|
-
'gemini-3-pro': { input: 2.00, output: 12.00 },
|
|
850
|
-
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
851
|
-
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
852
|
-
'gemini-2.0-flash-lite': { input: 0.02, output: 0.10 }
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Estimates the cost of sending a payload based on input token count and model pricing.
|
|
857
|
-
* NOTE: This only estimates INPUT cost. Output cost depends on response length and cannot be predicted.
|
|
858
|
-
* @this {ExportedAPI}
|
|
859
|
-
* @param {object|string} nextPayload - The next user message to be sent (object or string)
|
|
860
|
-
* @returns {Promise<Object>} - Cost estimation including input tokens, model, pricing, and estimated input cost
|
|
861
|
-
*/
|
|
862
|
-
async function estimateCost(nextPayload) {
|
|
863
|
-
const tokenInfo = await this.estimate(nextPayload);
|
|
864
|
-
const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
|
|
865
|
-
|
|
866
|
-
return {
|
|
867
|
-
inputTokens: tokenInfo.inputTokens,
|
|
868
|
-
model: this.modelName,
|
|
869
|
-
pricing: pricing,
|
|
870
|
-
estimatedInputCost: (tokenInfo.inputTokens / 1_000_000) * pricing.input,
|
|
871
|
-
note: 'Cost is for input tokens only; output cost depends on response length'
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Resets the current chat session, clearing all history and examples
|
|
878
|
-
* @this {ExportedAPI}
|
|
879
|
-
* @returns {Promise<void>}
|
|
880
|
-
*/
|
|
881
|
-
async function resetChat() {
|
|
882
|
-
if (this.chat) {
|
|
883
|
-
log.debug("Resetting Gemini chat session...");
|
|
884
|
-
|
|
885
|
-
// Prepare chat options with grounding if enabled
|
|
886
|
-
const chatOptions = {
|
|
887
|
-
model: this.modelName,
|
|
888
|
-
// @ts-ignore
|
|
889
|
-
config: {
|
|
890
|
-
...this.chatConfig,
|
|
891
|
-
...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
|
|
892
|
-
},
|
|
893
|
-
history: [],
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
// Only add tools if grounding is explicitly enabled
|
|
897
|
-
if (this.enableGrounding) {
|
|
898
|
-
chatOptions.config.tools = [{
|
|
899
|
-
googleSearch: this.groundingConfig
|
|
900
|
-
}];
|
|
901
|
-
log.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
this.chat = await this.genAIClient.chats.create(chatOptions);
|
|
905
|
-
log.debug("Chat session reset.");
|
|
906
|
-
} else {
|
|
907
|
-
log.warn("Cannot reset chat session: chat not yet initialized.");
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/**
|
|
912
|
-
* Retrieves the current conversation history for debugging or inspection
|
|
913
|
-
* @returns {Array<Object>} - An array of message objects in the conversation.
|
|
914
|
-
*/
|
|
915
|
-
function getChatHistory() {
|
|
916
|
-
if (!this.chat) {
|
|
917
|
-
log.warn("Chat session not initialized. No history available.");
|
|
918
|
-
return [];
|
|
919
|
-
}
|
|
920
|
-
return this.chat.getHistory();
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Updates system instructions and reinitializes the chat session
|
|
925
|
-
* @this {ExportedAPI}
|
|
926
|
-
* @param {string} newInstructions - The new system instructions
|
|
927
|
-
* @returns {Promise<void>}
|
|
928
|
-
*/
|
|
929
|
-
async function updateSystemInstructions(newInstructions) {
|
|
930
|
-
if (!newInstructions || typeof newInstructions !== 'string') {
|
|
931
|
-
throw new Error('System instructions must be a non-empty string');
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
this.systemInstructions = newInstructions.trim();
|
|
935
|
-
this.chatConfig.systemInstruction = this.systemInstructions;
|
|
936
|
-
|
|
937
|
-
log.debug('Updating system instructions and reinitializing chat...');
|
|
938
|
-
await this.init(true); // Force reinitialize with new instructions
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* Clears conversation history while preserving seeded examples.
|
|
943
|
-
* Useful for starting a fresh conversation within the same session
|
|
944
|
-
* without losing the few-shot learning examples.
|
|
945
|
-
* @this {ExportedAPI}
|
|
946
|
-
* @returns {Promise<void>}
|
|
947
|
-
*/
|
|
948
|
-
async function clearConversation() {
|
|
949
|
-
if (!this.chat) {
|
|
950
|
-
log.warn("Cannot clear conversation: chat not initialized.");
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const history = this.chat.getHistory();
|
|
955
|
-
const exampleHistory = history.slice(0, this.exampleCount || 0);
|
|
956
|
-
|
|
957
|
-
this.chat = await this.genAIClient.chats.create({
|
|
958
|
-
model: this.modelName,
|
|
959
|
-
// @ts-ignore
|
|
960
|
-
config: {
|
|
961
|
-
...this.chatConfig,
|
|
962
|
-
...(this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels })
|
|
963
|
-
},
|
|
964
|
-
history: exampleHistory,
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
// Reset usage tracking for the new conversation
|
|
968
|
-
this.lastResponseMetadata = null;
|
|
969
|
-
this._cumulativeUsage = {
|
|
970
|
-
promptTokens: 0,
|
|
971
|
-
responseTokens: 0,
|
|
972
|
-
totalTokens: 0,
|
|
973
|
-
attempts: 0
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
log.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* Returns structured usage data from the last message call for billing verification.
|
|
981
|
-
* Includes CUMULATIVE token counts across all retry attempts.
|
|
982
|
-
* Call this after message() or statelessMessage() to get actual token consumption.
|
|
4
|
+
* Exports:
|
|
5
|
+
* - Transformer — AI-powered JSON transformation via few-shot learning
|
|
6
|
+
* - Chat — Multi-turn text conversation with AI
|
|
7
|
+
* - Message — Stateless one-off messages to AI
|
|
8
|
+
* - ToolAgent — AI agent with user-provided tools
|
|
9
|
+
* - CodeAgent — AI agent that writes and executes code (stub)
|
|
10
|
+
* - BaseGemini — Base class for building custom wrappers
|
|
983
11
|
*
|
|
984
|
-
* @
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Sends a one-off message using generateContent (not chat).
|
|
1019
|
-
* Does NOT affect chat history - useful for isolated requests.
|
|
1020
|
-
* @this {ExportedAPI}
|
|
1021
|
-
* @param {Object|string} sourcePayload - The source payload.
|
|
1022
|
-
* @param {Object} [options] - Options including labels.
|
|
1023
|
-
* @param {AsyncValidatorFunction|null} [validatorFn] - Optional validator.
|
|
1024
|
-
* @returns {Promise<Object>} - The transformed payload.
|
|
1025
|
-
*/
|
|
1026
|
-
async function statelessMessage(sourcePayload, options = {}, validatorFn = null) {
|
|
1027
|
-
if (!this.chat) {
|
|
1028
|
-
throw new Error("Chat session not initialized. Please call init() first.");
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const payloadStr = typeof sourcePayload === 'string'
|
|
1032
|
-
? sourcePayload
|
|
1033
|
-
: JSON.stringify(sourcePayload, null, 2);
|
|
1034
|
-
|
|
1035
|
-
// Build contents including examples from current chat history
|
|
1036
|
-
const contents = [];
|
|
1037
|
-
|
|
1038
|
-
// Include seeded examples if we have them
|
|
1039
|
-
if (this.exampleCount > 0) {
|
|
1040
|
-
const history = this.chat.getHistory();
|
|
1041
|
-
const exampleHistory = history.slice(0, this.exampleCount);
|
|
1042
|
-
contents.push(...exampleHistory);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Add the user message
|
|
1046
|
-
contents.push({ role: 'user', parts: [{ text: payloadStr }] });
|
|
1047
|
-
|
|
1048
|
-
// Merge labels (Vertex AI only)
|
|
1049
|
-
const mergedLabels = { ...this.labels, ...(options.labels || {}) };
|
|
1050
|
-
|
|
1051
|
-
// Use generateContent instead of chat.sendMessage
|
|
1052
|
-
const result = await this.genAIClient.models.generateContent({
|
|
1053
|
-
model: this.modelName,
|
|
1054
|
-
contents: contents,
|
|
1055
|
-
config: {
|
|
1056
|
-
...this.chatConfig,
|
|
1057
|
-
...(this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels })
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
// Capture and log response metadata
|
|
1062
|
-
this.lastResponseMetadata = {
|
|
1063
|
-
modelVersion: result.modelVersion || null,
|
|
1064
|
-
requestedModel: this.modelName,
|
|
1065
|
-
promptTokens: result.usageMetadata?.promptTokenCount || 0,
|
|
1066
|
-
responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
|
|
1067
|
-
totalTokens: result.usageMetadata?.totalTokenCount || 0,
|
|
1068
|
-
timestamp: Date.now()
|
|
1069
|
-
};
|
|
1070
|
-
|
|
1071
|
-
// Set cumulative usage for stateless message (single attempt, no retries)
|
|
1072
|
-
this._cumulativeUsage = {
|
|
1073
|
-
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1074
|
-
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1075
|
-
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1076
|
-
attempts: 1
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
if (result.usageMetadata && log.level !== 'silent') {
|
|
1080
|
-
log.debug(`Stateless message metadata:`, {
|
|
1081
|
-
modelVersion: result.modelVersion || 'not-provided',
|
|
1082
|
-
promptTokens: result.usageMetadata.promptTokenCount,
|
|
1083
|
-
responseTokens: result.usageMetadata.candidatesTokenCount
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const modelResponse = result.text;
|
|
1088
|
-
const extractedJSON = extractJSON(modelResponse);
|
|
1089
|
-
|
|
1090
|
-
let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
|
|
1091
|
-
|
|
1092
|
-
// Validate if a validator is provided
|
|
1093
|
-
if (validatorFn) {
|
|
1094
|
-
await validatorFn(transformedPayload);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
return transformedPayload;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
/*
|
|
1102
|
-
----
|
|
1103
|
-
HELPERS
|
|
1104
|
-
----
|
|
1105
|
-
*/
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Attempts to recover truncated JSON by progressively removing characters from the end
|
|
1109
|
-
* until valid JSON is found or recovery fails
|
|
1110
|
-
* @param {string} text - The potentially truncated JSON string
|
|
1111
|
-
* @param {number} maxAttempts - Maximum number of characters to remove
|
|
1112
|
-
* @returns {Object|null} - Parsed JSON object or null if recovery fails
|
|
1113
|
-
*/
|
|
1114
|
-
function attemptJSONRecovery(text, maxAttempts = 100) {
|
|
1115
|
-
if (!text || typeof text !== 'string') return null;
|
|
1116
|
-
|
|
1117
|
-
// First, try parsing as-is
|
|
1118
|
-
try {
|
|
1119
|
-
return JSON.parse(text);
|
|
1120
|
-
} catch (e) {
|
|
1121
|
-
// Continue with recovery
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
let workingText = text.trim();
|
|
1125
|
-
|
|
1126
|
-
// First attempt: try to close unclosed structures without removing characters
|
|
1127
|
-
// Count open/close braces and brackets in the original text
|
|
1128
|
-
let braces = 0;
|
|
1129
|
-
let brackets = 0;
|
|
1130
|
-
let inString = false;
|
|
1131
|
-
let escapeNext = false;
|
|
1132
|
-
|
|
1133
|
-
for (let j = 0; j < workingText.length; j++) {
|
|
1134
|
-
const char = workingText[j];
|
|
1135
|
-
|
|
1136
|
-
if (escapeNext) {
|
|
1137
|
-
escapeNext = false;
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if (char === '\\') {
|
|
1142
|
-
escapeNext = true;
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
if (char === '"') {
|
|
1147
|
-
inString = !inString;
|
|
1148
|
-
continue;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
if (!inString) {
|
|
1152
|
-
if (char === '{') braces++;
|
|
1153
|
-
else if (char === '}') braces--;
|
|
1154
|
-
else if (char === '[') brackets++;
|
|
1155
|
-
else if (char === ']') brackets--;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Try to fix by just adding closing characters
|
|
1160
|
-
if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
|
|
1161
|
-
let fixedText = workingText;
|
|
1162
|
-
|
|
1163
|
-
// Close any open strings first
|
|
1164
|
-
if (inString) {
|
|
1165
|
-
fixedText += '"';
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Add missing closing characters
|
|
1169
|
-
while (braces > 0) {
|
|
1170
|
-
fixedText += '}';
|
|
1171
|
-
braces--;
|
|
1172
|
-
}
|
|
1173
|
-
while (brackets > 0) {
|
|
1174
|
-
fixedText += ']';
|
|
1175
|
-
brackets--;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
try {
|
|
1179
|
-
const result = JSON.parse(fixedText);
|
|
1180
|
-
if (log.level !== 'silent') {
|
|
1181
|
-
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
1182
|
-
}
|
|
1183
|
-
return result;
|
|
1184
|
-
} catch (e) {
|
|
1185
|
-
// Simple fix didn't work, continue with more aggressive recovery
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// Second attempt: progressively remove characters from the end
|
|
1190
|
-
|
|
1191
|
-
for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
|
|
1192
|
-
// Remove one character from the end
|
|
1193
|
-
workingText = workingText.slice(0, -1);
|
|
1194
|
-
|
|
1195
|
-
// Count open/close braces and brackets
|
|
1196
|
-
let braces = 0;
|
|
1197
|
-
let brackets = 0;
|
|
1198
|
-
let inString = false;
|
|
1199
|
-
let escapeNext = false;
|
|
1200
|
-
|
|
1201
|
-
for (let j = 0; j < workingText.length; j++) {
|
|
1202
|
-
const char = workingText[j];
|
|
1203
|
-
|
|
1204
|
-
if (escapeNext) {
|
|
1205
|
-
escapeNext = false;
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (char === '\\') {
|
|
1210
|
-
escapeNext = true;
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (char === '"') {
|
|
1215
|
-
inString = !inString;
|
|
1216
|
-
continue;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
if (!inString) {
|
|
1220
|
-
if (char === '{') braces++;
|
|
1221
|
-
else if (char === '}') braces--;
|
|
1222
|
-
else if (char === '[') brackets++;
|
|
1223
|
-
else if (char === ']') brackets--;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// If we have balanced braces/brackets, try parsing
|
|
1228
|
-
if (braces === 0 && brackets === 0 && !inString) {
|
|
1229
|
-
try {
|
|
1230
|
-
const result = JSON.parse(workingText);
|
|
1231
|
-
if (log.level !== 'silent') {
|
|
1232
|
-
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
|
|
1233
|
-
}
|
|
1234
|
-
return result;
|
|
1235
|
-
} catch (e) {
|
|
1236
|
-
// Continue trying
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// After a few attempts, try adding closing characters
|
|
1241
|
-
if (i > 5) {
|
|
1242
|
-
let fixedText = workingText;
|
|
1243
|
-
|
|
1244
|
-
// Close any open strings first
|
|
1245
|
-
if (inString) {
|
|
1246
|
-
fixedText += '"';
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Add missing closing characters
|
|
1250
|
-
while (braces > 0) {
|
|
1251
|
-
fixedText += '}';
|
|
1252
|
-
braces--;
|
|
1253
|
-
}
|
|
1254
|
-
while (brackets > 0) {
|
|
1255
|
-
fixedText += ']';
|
|
1256
|
-
brackets--;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
try {
|
|
1260
|
-
const result = JSON.parse(fixedText);
|
|
1261
|
-
if (log.level !== 'silent') {
|
|
1262
|
-
log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
|
|
1263
|
-
}
|
|
1264
|
-
return result;
|
|
1265
|
-
} catch (e) {
|
|
1266
|
-
// Recovery failed, continue trying
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
return null;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
function isJSON(data) {
|
|
1275
|
-
try {
|
|
1276
|
-
const attempt = JSON.stringify(data);
|
|
1277
|
-
if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
|
|
1278
|
-
if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
|
|
1279
|
-
return true;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
return false;
|
|
1283
|
-
} catch (e) {
|
|
1284
|
-
return false;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
function isJSONStr(string) {
|
|
1289
|
-
if (typeof string !== 'string') return false;
|
|
1290
|
-
try {
|
|
1291
|
-
const result = JSON.parse(string);
|
|
1292
|
-
const type = Object.prototype.toString.call(result);
|
|
1293
|
-
return type === '[object Object]' || type === '[object Array]';
|
|
1294
|
-
} catch (err) {
|
|
1295
|
-
return false;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
function extractJSON(text) {
|
|
1300
|
-
if (!text || typeof text !== 'string') {
|
|
1301
|
-
throw new Error('No text provided for JSON extraction');
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Strategy 1: Try parsing the entire response as JSON
|
|
1305
|
-
if (isJSONStr(text.trim())) {
|
|
1306
|
-
return JSON.parse(text.trim());
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// Strategy 2: Look for JSON code blocks (```json...``` or ```...```)
|
|
1310
|
-
const codeBlockPatterns = [
|
|
1311
|
-
/```json\s*\n?([\s\S]*?)\n?\s*```/gi,
|
|
1312
|
-
/```\s*\n?([\s\S]*?)\n?\s*```/gi
|
|
1313
|
-
];
|
|
1314
|
-
|
|
1315
|
-
for (const pattern of codeBlockPatterns) {
|
|
1316
|
-
const matches = text.match(pattern);
|
|
1317
|
-
if (matches) {
|
|
1318
|
-
for (const match of matches) {
|
|
1319
|
-
const jsonContent = match.replace(/```json\s*\n?/gi, '').replace(/```\s*\n?/gi, '').trim();
|
|
1320
|
-
if (isJSONStr(jsonContent)) {
|
|
1321
|
-
return JSON.parse(jsonContent);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// Strategy 3: Look for JSON objects/arrays using bracket matching
|
|
1328
|
-
const jsonPatterns = [
|
|
1329
|
-
// Match complete JSON objects
|
|
1330
|
-
/\{[\s\S]*\}/g,
|
|
1331
|
-
// Match complete JSON arrays
|
|
1332
|
-
/\[[\s\S]*\]/g
|
|
1333
|
-
];
|
|
1334
|
-
|
|
1335
|
-
for (const pattern of jsonPatterns) {
|
|
1336
|
-
const matches = text.match(pattern);
|
|
1337
|
-
if (matches) {
|
|
1338
|
-
for (const match of matches) {
|
|
1339
|
-
const candidate = match.trim();
|
|
1340
|
-
if (isJSONStr(candidate)) {
|
|
1341
|
-
return JSON.parse(candidate);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// Strategy 4: Advanced bracket matching for nested structures
|
|
1348
|
-
const advancedExtract = findCompleteJSONStructures(text);
|
|
1349
|
-
if (advancedExtract.length > 0) {
|
|
1350
|
-
// Return the first valid JSON structure found
|
|
1351
|
-
for (const candidate of advancedExtract) {
|
|
1352
|
-
if (isJSONStr(candidate)) {
|
|
1353
|
-
return JSON.parse(candidate);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// Strategy 5: Clean up common formatting issues and retry
|
|
1359
|
-
const cleanedText = text
|
|
1360
|
-
.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, '') // Remove conversational intros
|
|
1361
|
-
.replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, '')
|
|
1362
|
-
.replace(/^\s*The\s+.*?is\s*[:\n]/gi, '')
|
|
1363
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* comments */
|
|
1364
|
-
.replace(/\/\/.*$/gm, '') // Remove // comments
|
|
1365
|
-
.trim();
|
|
1366
|
-
|
|
1367
|
-
if (isJSONStr(cleanedText)) {
|
|
1368
|
-
return JSON.parse(cleanedText);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// Strategy 6: Last resort - attempt recovery for potentially truncated JSON
|
|
1372
|
-
// This is especially useful when maxOutputTokens might have cut off the response
|
|
1373
|
-
const recoveredJSON = attemptJSONRecovery(text);
|
|
1374
|
-
if (recoveredJSON !== null) {
|
|
1375
|
-
return recoveredJSON;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
// If all else fails, throw an error with helpful information
|
|
1379
|
-
throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function findCompleteJSONStructures(text) {
|
|
1383
|
-
const results = [];
|
|
1384
|
-
const startChars = ['{', '['];
|
|
1385
|
-
|
|
1386
|
-
for (let i = 0; i < text.length; i++) {
|
|
1387
|
-
if (startChars.includes(text[i])) {
|
|
1388
|
-
const extracted = extractCompleteStructure(text, i);
|
|
1389
|
-
if (extracted) {
|
|
1390
|
-
results.push(extracted);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
return results;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
function extractCompleteStructure(text, startPos) {
|
|
1400
|
-
const startChar = text[startPos];
|
|
1401
|
-
const endChar = startChar === '{' ? '}' : ']';
|
|
1402
|
-
let depth = 0;
|
|
1403
|
-
let inString = false;
|
|
1404
|
-
let escaped = false;
|
|
1405
|
-
|
|
1406
|
-
for (let i = startPos; i < text.length; i++) {
|
|
1407
|
-
const char = text[i];
|
|
1408
|
-
|
|
1409
|
-
if (escaped) {
|
|
1410
|
-
escaped = false;
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
if (char === '\\' && inString) {
|
|
1415
|
-
escaped = true;
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
if (char === '"' && !escaped) {
|
|
1420
|
-
inString = !inString;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
if (!inString) {
|
|
1425
|
-
if (char === startChar) {
|
|
1426
|
-
depth++;
|
|
1427
|
-
} else if (char === endChar) {
|
|
1428
|
-
depth--;
|
|
1429
|
-
if (depth === 0) {
|
|
1430
|
-
return text.substring(startPos, i + 1);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
return null; // Incomplete structure
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
1440
|
-
log.info("RUNNING AI Transformer as standalone script...");
|
|
1441
|
-
(
|
|
1442
|
-
async () => {
|
|
1443
|
-
try {
|
|
1444
|
-
log.info("Initializing AI Transformer...");
|
|
1445
|
-
const transformer = new AITransformer({
|
|
1446
|
-
modelName: 'gemini-2.5-flash',
|
|
1447
|
-
sourceKey: 'INPUT', // Custom source key
|
|
1448
|
-
targetKey: 'OUTPUT', // Custom target key
|
|
1449
|
-
contextKey: 'CONTEXT', // Custom context key
|
|
1450
|
-
maxRetries: 2,
|
|
1451
|
-
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
const examples = [
|
|
1455
|
-
{
|
|
1456
|
-
CONTEXT: "Generate professional profiles with emoji representations",
|
|
1457
|
-
INPUT: { "name": "Alice" },
|
|
1458
|
-
OUTPUT: { "name": "Alice", "profession": "data scientist", "life_as_told_by_emoji": ["🔬", "💡", "📊", "🧠", "🌟"] }
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
INPUT: { "name": "Bob" },
|
|
1462
|
-
OUTPUT: { "name": "Bob", "profession": "product manager", "life_as_told_by_emoji": ["📋", "🤝", "🚀", "💬", "🎯"] }
|
|
1463
|
-
},
|
|
1464
|
-
{
|
|
1465
|
-
INPUT: { "name": "Eve" },
|
|
1466
|
-
OUTPUT: { "name": "Even", "profession": "security analyst", "life_as_told_by_emoji": ["🕵️♀️", "🔒", "💻", "👀", "⚡️"] }
|
|
1467
|
-
},
|
|
1468
|
-
];
|
|
1469
|
-
|
|
1470
|
-
await transformer.init();
|
|
1471
|
-
await transformer.seed(examples);
|
|
1472
|
-
log.info("AI Transformer initialized and seeded with examples.");
|
|
1473
|
-
|
|
1474
|
-
// Test normal transformation
|
|
1475
|
-
const normalResponse = await transformer.message({ "name": "AK" });
|
|
1476
|
-
log.info("Normal Payload Transformed", normalResponse);
|
|
1477
|
-
|
|
1478
|
-
// Test transformation with validation
|
|
1479
|
-
const mockValidator = async (payload) => {
|
|
1480
|
-
// Simulate validation logic
|
|
1481
|
-
if (!payload.profession || !payload.life_as_told_by_emoji) {
|
|
1482
|
-
throw new Error("Missing required fields: profession or life_as_told_by_emoji");
|
|
1483
|
-
}
|
|
1484
|
-
if (!Array.isArray(payload.life_as_told_by_emoji)) {
|
|
1485
|
-
throw new Error("life_as_told_by_emoji must be an array");
|
|
1486
|
-
}
|
|
1487
|
-
return payload; // Return the payload if validation passes
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
const validatedResponse = await transformer.messageAndValidate(
|
|
1491
|
-
{ "name": "Lynn" },
|
|
1492
|
-
{},
|
|
1493
|
-
mockValidator
|
|
1494
|
-
);
|
|
1495
|
-
log.info("Validated Payload Transformed", validatedResponse);
|
|
1496
|
-
|
|
1497
|
-
if (NODE_ENV === 'dev') debugger;
|
|
1498
|
-
} catch (error) {
|
|
1499
|
-
log.error("Error in AI Transformer script:", error);
|
|
1500
|
-
if (NODE_ENV === 'dev') debugger;
|
|
1501
|
-
}
|
|
1502
|
-
})();
|
|
1503
|
-
}
|
|
12
|
+
* @example
|
|
13
|
+
* ```javascript
|
|
14
|
+
* import { Transformer, Chat, Message, ToolAgent } from 'ak-gemini';
|
|
15
|
+
* // or
|
|
16
|
+
* import AI from 'ak-gemini';
|
|
17
|
+
* const t = new AI.Transformer({ ... });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ── Named Exports ──
|
|
22
|
+
|
|
23
|
+
export { default as Transformer } from './transformer.js';
|
|
24
|
+
export { default as Chat } from './chat.js';
|
|
25
|
+
export { default as Message } from './message.js';
|
|
26
|
+
export { default as ToolAgent } from './tool-agent.js';
|
|
27
|
+
export { default as CodeAgent } from './code-agent.js';
|
|
28
|
+
export { default as BaseGemini } from './base.js';
|
|
29
|
+
export { default as log } from './logger.js';
|
|
30
|
+
export { ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
31
|
+
export { extractJSON, attemptJSONRecovery } from './json-helpers.js';
|
|
32
|
+
|
|
33
|
+
// ── Default Export (namespace object) ──
|
|
34
|
+
|
|
35
|
+
import Transformer from './transformer.js';
|
|
36
|
+
import Chat from './chat.js';
|
|
37
|
+
import Message from './message.js';
|
|
38
|
+
import ToolAgent from './tool-agent.js';
|
|
39
|
+
import CodeAgent from './code-agent.js';
|
|
40
|
+
|
|
41
|
+
export default { Transformer, Chat, Message, ToolAgent, CodeAgent };
|