ak-gemini 1.0.4 → 1.0.5

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.
Files changed (4) hide show
  1. package/README.md +40 -20
  2. package/index.cjs +23 -2
  3. package/index.js +45 -2
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -7,12 +7,13 @@ Use this to power LLM-driven data pipelines, JSON mapping, or any automated AI t
7
7
 
8
8
  ## Features
9
9
 
10
- * **Model-Agnostic**: Configure for any Gemini model (`gemini-2.0-flash` by default)
11
- * **Declarative Examples**: Seed transformations using example mappings, with support for custom keys (`PROMPT`, `ANSWER`, `CONTEXT`, or your own)
12
- * **Automatic Validation & Repair**: Validate outputs with your own async function; auto-repair failed payloads with LLM feedback loop (exponential backoff, fully configurable)
13
- * **Strong TypeScript/JSDoc Typings**: All public APIs fully typed (see `/types`)
14
- * **Minimal API Surface**: Dead simple, no ceremony—init, seed, transform, validate.
15
- * **Robust Logging**: Pluggable logger for all steps, easy debugging
10
+ * **Model-Agnostic:** Use any Gemini model (`gemini-2.0-flash` by default)
11
+ * **Declarative Few-shot Examples:** Seed transformations using example mappings, with support for custom keys (`PROMPT`, `ANSWER`, `CONTEXT`, or your own)
12
+ * **Automatic Validation & Repair:** Validate outputs with your own async function; auto-repair failed payloads with LLM feedback loop (exponential backoff, fully configurable)
13
+ * **Token Counting & Safety:** Preview the *exact* Gemini token consumption for any operation—including all examples, instructions, and your input—before sending, so you can avoid window errors and manage costs.
14
+ * **Strong TypeScript/JSDoc Typings:** All public APIs fully typed (see `/types`)
15
+ * **Minimal API Surface:** Dead simple, no ceremony—init, seed, transform, validate.
16
+ * **Robust Logging:** Pluggable logger for all steps, easy debugging
16
17
 
17
18
  ---
18
19
 
@@ -43,7 +44,7 @@ or pass it directly in the constructor options.
43
44
  ### 2. **Basic Example**
44
45
 
45
46
  ```js
46
- import AITransformer from 'ai-transformer';
47
+ import AITransformer from 'ak-gemini';
47
48
 
48
49
  const transformer = new AITransformer({
49
50
  modelName: 'gemini-2.0-flash', // or your preferred Gemini model
@@ -72,7 +73,22 @@ console.log(result);
72
73
 
73
74
  ---
74
75
 
75
- ### 3. **Automatic Validation & Self-Healing**
76
+ ### 3. **Token Window Safety/Preview**
77
+
78
+ Before calling `.message()` or `.seed()`, you can preview the exact token usage that will be sent to Gemini—*including* your system instructions, examples, and user input. This is vital for avoiding window errors and managing context size:
79
+
80
+ ```js
81
+ const { totalTokens, breakdown } = await transformer.estimateTokenUsage({ name: "Bob" });
82
+ console.log(`Total tokens: ${totalTokens}`);
83
+ console.log(breakdown); // See per-section token counts
84
+
85
+ // Optional: abort or trim if over limit
86
+ if (totalTokens > 32000) throw new Error("Request too large for selected Gemini model");
87
+ ```
88
+
89
+ ---
90
+
91
+ ### 4. **Automatic Validation & Self-Healing**
76
92
 
77
93
  You can pass a custom async validator—if it fails, the transformer will attempt to self-correct using LLM feedback, retrying up to `maxRetries` times:
78
94
 
@@ -127,7 +143,12 @@ You can omit `examples` to use the `examplesFile` (if provided).
127
143
 
128
144
  #### `await transformer.message(sourcePayload)`
129
145
 
130
- Transforms input JSON to output JSON using the seeded examples and system instructions.
146
+ Transforms input JSON to output JSON using the seeded examples and system instructions. Throws if estimated token window would be exceeded.
147
+
148
+ #### `await transformer.estimateTokenUsage(sourcePayload)`
149
+
150
+ Returns `{ totalTokens, breakdown }` for the *full request* that would be sent to Gemini (system instructions + all examples + your sourcePayload as the new prompt).
151
+ Lets you preview token window safety and abort/trim as needed.
131
152
 
132
153
  #### `await transformer.transformWithValidation(sourcePayload, validatorFn, options?)`
133
154
 
@@ -187,10 +208,19 @@ const result = await transformer.transformWithValidation(
187
208
 
188
209
  ---
189
210
 
211
+ ## Token Window Management & Error Handling
212
+
213
+ * Throws on missing `GEMINI_API_KEY`
214
+ * `.message()` and `.seed()` will *estimate* and prevent calls that would exceed Gemini's model window
215
+ * All API and parsing errors surfaced as `Error` with context
216
+ * Validator and retry failures include the number of attempts and last error
217
+
218
+ ---
219
+
190
220
  ## Testing
191
221
 
192
222
  * **Jest test suite included**
193
- * Mocks Google Gemini, logger, ak-tools
223
+ * Real API integration tests as well as local unit tests
194
224
  * 100% coverage for all error cases, configuration options, edge cases
195
225
 
196
226
  Run tests with:
@@ -200,13 +230,3 @@ npm test
200
230
  ```
201
231
 
202
232
  ---
203
-
204
- ## Error Handling
205
-
206
- * Throws on missing `GEMINI_API_KEY`
207
- * All API and parsing errors surfaced as `Error` with context
208
- * Validator and retry failures include the number of attempts and last error
209
-
210
- ---
211
-
212
-
package/index.cjs CHANGED
@@ -108,6 +108,7 @@ var AITransformer = class {
108
108
  this.reset = resetChat.bind(this);
109
109
  this.getHistory = getChatHistory.bind(this);
110
110
  this.transformWithValidation = transformWithValidation.bind(this);
111
+ this.estimate = estimateTokenUsage.bind(this);
111
112
  }
112
113
  };
113
114
  function AITransformFactory(options = {}) {
@@ -135,7 +136,8 @@ function AITransformFactory(options = {}) {
135
136
  }
136
137
  logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
137
138
  logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
138
- this.genAIClient = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
139
+ const ai = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
140
+ this.genAIClient = ai;
139
141
  this.chat = null;
140
142
  }
141
143
  async function initChat() {
@@ -186,7 +188,7 @@ async function seedWithExamples(examples) {
186
188
  historyToAdd.push({ role: "model", parts: [{ text: answerText }] });
187
189
  }
188
190
  }
189
- const currentHistory = this.chat.getHistory();
191
+ const currentHistory = this?.chat?.getHistory() || [];
190
192
  this.chat = await this.genAIClient.chats.create({
191
193
  model: this.modelName,
192
194
  // @ts-ignore
@@ -246,6 +248,25 @@ async function transformWithValidation(sourcePayload, validatorFn, options = {})
246
248
  }
247
249
  }
248
250
  }
251
+ async function estimateTokenUsage(nextPayload) {
252
+ const contents = [];
253
+ if (this.systemInstructions) {
254
+ contents.push({ parts: [{ text: this.systemInstructions }] });
255
+ }
256
+ if (this.chat && typeof this.chat.getHistory === "function") {
257
+ const history = this.chat.getHistory();
258
+ if (Array.isArray(history) && history.length > 0) {
259
+ contents.push(...history);
260
+ }
261
+ }
262
+ const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
263
+ contents.push({ parts: [{ text: nextMessage }] });
264
+ const resp = await this.genAIClient.models.countTokens({
265
+ model: this.modelName,
266
+ contents
267
+ });
268
+ return resp;
269
+ }
249
270
  async function rebuildPayload(lastPayload, serverError) {
250
271
  await this.init();
251
272
  const prompt = `
package/index.js CHANGED
@@ -96,6 +96,7 @@ export default class AITransformer {
96
96
  this.reset = resetChat.bind(this);
97
97
  this.getHistory = getChatHistory.bind(this);
98
98
  this.transformWithValidation = transformWithValidation.bind(this);
99
+ this.estimate = estimateTokenUsage.bind(this);
99
100
  }
100
101
  }
101
102
 
@@ -143,7 +144,8 @@ function AITransformFactory(options = {}) {
143
144
  log.debug(`Creating AI Transformer with model: ${this.modelName}`);
144
145
  log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
145
146
 
146
- this.genAIClient = new GoogleGenAI({ apiKey: this.apiKey });
147
+ const ai = new GoogleGenAI({ apiKey: this.apiKey });
148
+ this.genAIClient = ai;
147
149
  this.chat = null;
148
150
  }
149
151
 
@@ -221,7 +223,7 @@ async function seedWithExamples(examples) {
221
223
  }
222
224
  }
223
225
 
224
- const currentHistory = this.chat.getHistory();
226
+ const currentHistory = this?.chat?.getHistory() || [];
225
227
 
226
228
  this.chat = await this.genAIClient.chats.create({
227
229
  model: this.modelName,
@@ -317,6 +319,47 @@ async function transformWithValidation(sourcePayload, validatorFn, options = {})
317
319
  }
318
320
  }
319
321
 
322
+
323
+ /**
324
+ * Estimate total token usage if you were to send a new payload as the next message.
325
+ * Considers system instructions, current chat history (including examples), and the new message.
326
+ * @param {object|string} nextPayload - The next user message to be sent (object or string)
327
+ * @returns {Promise<{ totalTokens: number, ... }>} - The result of Gemini's countTokens API
328
+ */
329
+ async function estimateTokenUsage(nextPayload) {
330
+ // Compose the conversation contents, Gemini-style
331
+ const contents = [];
332
+
333
+ // (1) System instructions (if applicable)
334
+ if (this.systemInstructions) {
335
+ // Add as a 'system' part; adjust role if Gemini supports
336
+ contents.push({ parts: [{ text: this.systemInstructions }] });
337
+ }
338
+
339
+ // (2) All current chat history (seeded examples + real user/model turns)
340
+ if (this.chat && typeof this.chat.getHistory === "function") {
341
+ const history = this.chat.getHistory();
342
+ if (Array.isArray(history) && history.length > 0) {
343
+ contents.push(...history);
344
+ }
345
+ }
346
+
347
+ // (3) The next user message
348
+ const nextMessage = typeof nextPayload === "string"
349
+ ? nextPayload
350
+ : JSON.stringify(nextPayload, null, 2);
351
+
352
+ contents.push({ parts: [{ text: nextMessage }] });
353
+
354
+ // Call Gemini's token estimator
355
+ const resp = await this.genAIClient.models.countTokens({
356
+ model: this.modelName,
357
+ contents,
358
+ });
359
+
360
+ return resp; // includes totalTokens, possibly breakdown
361
+ }
362
+
320
363
  /**
321
364
  * Rebuilds a payload based on server error feedback
322
365
  * @param {Object} lastPayload - The payload that failed validation
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ak-gemini",
3
3
  "author": "ak@mixpanel.com",
4
4
  "description": "AK's Generative AI Helper for doing... transforms",
5
- "version": "1.0.4",
5
+ "version": "1.0.5",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
@@ -47,7 +47,7 @@
47
47
  "license": "ISC",
48
48
  "dependencies": {
49
49
  "@google-cloud/functions-framework": "^4.0.0",
50
- "@google/genai": "^1.3.0",
50
+ "@google/genai": "^1.4.0",
51
51
  "ak-tools": "^1.0.64",
52
52
  "dotenv": "^16.5.0",
53
53
  "pino": "^9.7.0",