@tstdl/base 0.92.3 → 0.92.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.
@@ -1,36 +1,53 @@
1
1
  import '../polyfills.js';
2
2
  import { GoogleGenerativeAI } from '@google/generative-ai';
3
3
  import { type FileMetadataResponse, GoogleAIFileManager } from '@google/generative-ai/server';
4
+ import { LiteralUnion } from 'type-fest';
4
5
  import { Resolvable, type resolveArgumentType } from '../injector/interfaces.js';
5
6
  import { OneOrMany, SchemaTestable } from '../schema/index.js';
6
7
  import { Enumeration as EnumerationType, EnumerationValue } from '../types.js';
7
- import { LiteralUnion } from 'type-fest';
8
8
  export type FileInput = {
9
9
  path: string;
10
10
  mimeType: string;
11
11
  } | Blob;
12
12
  export type GenerativeAIModel = LiteralUnion<'gemini-2.0-flash-exp' | 'gemini-exp-1206' | 'gemini-2.0-flash-thinking-exp-1219', string>;
13
+ export type GenerationOptions = {
14
+ model?: GenerativeAIModel;
15
+ maxOutputTokens?: number;
16
+ temperature?: number;
17
+ topP?: number;
18
+ topK?: number;
19
+ presencePenalty?: number;
20
+ frequencyPenalty?: number;
21
+ };
22
+ export type GenerationResult<T> = {
23
+ result: T;
24
+ usage?: {
25
+ promptTokenCount: number;
26
+ candidatesTokenCount: number;
27
+ totalTokenCount: number;
28
+ };
29
+ };
13
30
  export type AiServiceOptions = {
14
31
  apiKey: string;
15
- model?: GenerativeAIModel;
32
+ defaultModel?: GenerativeAIModel;
16
33
  };
17
34
  export type AiServiceArgument = AiServiceOptions;
18
35
  export declare class AiService implements Resolvable<AiServiceArgument> {
19
36
  #private;
20
37
  readonly genAI: GoogleGenerativeAI;
21
38
  readonly fileManager: GoogleAIFileManager;
22
- readonly model: import("@google/generative-ai").GenerativeModel;
39
+ readonly defaultModel: GenerativeAIModel;
23
40
  readonly [resolveArgumentType]: AiServiceArgument;
24
41
  getFile(fileInput: FileInput): Promise<FileMetadataResponse>;
25
42
  getFiles(files: readonly FileInput[]): Promise<FileMetadataResponse[]>;
26
- classify<T extends EnumerationType>(fileInput: OneOrMany<FileInput>, types: T): Promise<{
27
- reasoning: string;
43
+ classify<T extends EnumerationType>(fileInput: OneOrMany<FileInput>, types: T, options?: GenerationOptions): Promise<GenerationResult<{
28
44
  types: {
29
45
  type: EnumerationValue<T>;
30
46
  confidence: 'high' | 'medium' | 'low';
31
47
  }[] | null;
32
- }>;
33
- extractData<T>(fileInput: OneOrMany<FileInput>, schema: SchemaTestable<T>): Promise<T>;
34
- waitForFileActive(fileMetadata: FileMetadataResponse): Promise<FileMetadataResponse>;
35
- waitForFilesActive(...files: FileMetadataResponse[]): Promise<FileMetadataResponse[]>;
48
+ }>>;
49
+ extractData<T>(fileInput: OneOrMany<FileInput>, schema: SchemaTestable<T>, options?: GenerationOptions): Promise<GenerationResult<T>>;
50
+ private waitForFileActive;
51
+ private waitForFilesActive;
52
+ private getModel;
36
53
  }
package/ai/ai.service.js CHANGED
@@ -65,11 +65,13 @@ import { GoogleGenerativeAI } from '@google/generative-ai';
65
65
  import { FileState, GoogleAIFileManager } from '@google/generative-ai/server';
66
66
  import { DetailsError } from '../errors/details.error.js';
67
67
  import { Singleton } from '../injector/decorators.js';
68
- import { injectArgument } from '../injector/inject.js';
68
+ import { inject, injectArgument } from '../injector/inject.js';
69
+ import { Logger } from '../logger/logger.js';
69
70
  import { convertToOpenApiSchema } from '../schema/converters/openapi-converter.js';
70
- import { array, enumeration, nullable, object, Schema, string } from '../schema/index.js';
71
+ import { array, enumeration, nullable, object, Schema } from '../schema/index.js';
71
72
  import { toArray } from '../utils/array/array.js';
72
73
  import { digest } from '../utils/cryptography.js';
74
+ import { formatBytes } from '../utils/format.js';
73
75
  import { timeout } from '../utils/timing.js';
74
76
  import { tryIgnoreAsync } from '../utils/try-ignore.js';
75
77
  import { isBlob } from '../utils/type-guards.js';
@@ -77,19 +79,21 @@ import { millisecondsPerSecond } from '../utils/units.js';
77
79
  let AiService = class AiService {
78
80
  #options = injectArgument(this);
79
81
  #fileCache = new Map();
82
+ #logger = inject(Logger, 'AiService');
80
83
  genAI = new GoogleGenerativeAI(this.#options.apiKey);
81
84
  fileManager = new GoogleAIFileManager(this.#options.apiKey);
82
- model = this.genAI.getGenerativeModel({ model: this.#options.model ?? 'gemini-2.0-flash-exp' });
85
+ defaultModel = this.#options.defaultModel ?? 'gemini-2.0-flash-exp';
83
86
  async getFile(fileInput) {
84
87
  const path = isBlob(fileInput) ? join(tmpdir(), crypto.randomUUID()) : fileInput.path;
85
88
  const mimeType = isBlob(fileInput) ? fileInput.type : fileInput.mimeType;
86
89
  const blob = isBlob(fileInput) ? fileInput : await openAsBlob(path, { type: mimeType });
87
90
  const buffer = await blob.arrayBuffer();
88
91
  const byteArray = new Uint8Array(buffer);
89
- const fileHash = await digest('SHA-256', byteArray).toBase64();
92
+ const fileHash = await digest('SHA-1', byteArray).toBase64();
90
93
  const fileKey = `${fileHash}:${byteArray.length}`;
91
94
  if (this.#fileCache.has(fileKey)) {
92
95
  try {
96
+ this.#logger.verbose(`Fetching file "${fileHash}" from cache...`);
93
97
  const cachedFile = await this.#fileCache.get(fileKey);
94
98
  return await this.fileManager.getFile(cachedFile.name);
95
99
  }
@@ -106,7 +110,9 @@ let AiService = class AiService {
106
110
  stack.defer(async () => tryIgnoreAsync(async () => unlink(path)));
107
111
  await writeFile(path, byteArray);
108
112
  }
113
+ this.#logger.verbose(`Uploading file "${fileHash}" (${formatBytes(byteArray.length)})...`);
109
114
  const result = await this.fileManager.uploadFile(path, { mimeType });
115
+ this.#logger.verbose(`Processing file "${fileHash}"...`);
110
116
  return await this.waitForFileActive(result.file);
111
117
  }
112
118
  catch (e_1) {
@@ -130,20 +136,21 @@ let AiService = class AiService {
130
136
  async getFiles(files) {
131
137
  return Promise.all(files.map(async (file) => this.getFile(file)));
132
138
  }
133
- async classify(fileInput, types) {
139
+ async classify(fileInput, types, options) {
134
140
  const files = await this.getFiles(toArray(fileInput));
135
141
  const resultSchema = object({
136
- reasoning: string({ description: 'Reasoning for classification. Use to be more confident, if unsure. Reason for every somewhat likely document type.' }),
137
142
  types: nullable(array(object({
138
143
  type: enumeration(types, { description: 'Type of document' }),
139
144
  confidence: enumeration(['high', 'medium', 'low'], { description: 'How sure/certain you are about the classficiation.' })
140
145
  }), { description: 'One or more document types that matches' }))
141
146
  });
142
147
  const responseSchema = convertToOpenApiSchema(resultSchema);
143
- const result = await this.model.generateContent({
148
+ this.#logger.verbose('Classifying...');
149
+ const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
144
150
  generationConfig: {
145
- maxOutputTokens: 1024,
146
- temperature: 0.5,
151
+ maxOutputTokens: 2048,
152
+ temperature: 0.75,
153
+ ...options,
147
154
  responseMimeType: 'application/json',
148
155
  responseSchema
149
156
  },
@@ -158,27 +165,27 @@ let AiService = class AiService {
158
165
  }
159
166
  ]
160
167
  });
161
- return resultSchema.parse(JSON.parse(result.response.text()));
168
+ return {
169
+ usage: result.response.usageMetadata,
170
+ result: resultSchema.parse(JSON.parse(result.response.text()))
171
+ };
162
172
  }
163
- async extractData(fileInput, schema) {
173
+ async extractData(fileInput, schema, options) {
164
174
  const files = await this.getFiles(toArray(fileInput));
165
175
  const responseSchema = convertToOpenApiSchema(schema);
166
- const result = await this.model.generateContent({
176
+ this.#logger.verbose('Extracting data...');
177
+ const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
167
178
  generationConfig: {
168
- maxOutputTokens: 4096,
169
- temperature: 0.5,
179
+ maxOutputTokens: 8192,
180
+ temperature: 0.75,
181
+ ...options,
170
182
  responseMimeType: 'application/json',
171
183
  responseSchema
172
184
  },
173
185
  systemInstruction: `You are a highly skilled data extraction AI, specializing in accurately identifying and extracting information from unstructured text documents and converting it into a structured JSON format. Your primary goal is to meticulously follow the provided JSON schema and populate it with data extracted from the given document.
174
186
 
175
187
  **Instructions:**
176
- Carefully read and analyze the provided document. Identify relevant information that corresponds to each field in the JSON schema. Focus on accuracy and avoid making assumptions. If a field has multiple possible values, extract all relevant ones into the correct array structures ONLY IF the schema defines that field as an array; otherwise, extract only the single most relevant value.
177
-
178
- **Reasoning**
179
- Reason about every field in the json schema and find the best matching value. If there are multiple relevant values but the data type is not an array, reason about the values to find out which is the most relevant one.
180
-
181
- You *MUST* output the reasoning first.`,
188
+ Carefully read and analyze the provided document. Identify relevant information that corresponds to each field in the JSON schema. Focus on accuracy and avoid making assumptions. If a field has multiple possible values, extract all relevant ones into the correct array structures ONLY IF the schema defines that field as an array; otherwise, extract only the single most relevant value.`,
182
189
  contents: [
183
190
  {
184
191
  role: 'user',
@@ -189,7 +196,10 @@ You *MUST* output the reasoning first.`,
189
196
  }
190
197
  ]
191
198
  });
192
- return Schema.parse(schema, JSON.parse(result.response.text()));
199
+ return {
200
+ usage: result.response.usageMetadata,
201
+ result: Schema.parse(schema, JSON.parse(result.response.text()))
202
+ };
193
203
  }
194
204
  async waitForFileActive(fileMetadata) {
195
205
  let file = await this.fileManager.getFile(fileMetadata.name);
@@ -210,6 +220,9 @@ You *MUST* output the reasoning first.`,
210
220
  }
211
221
  return responses;
212
222
  }
223
+ getModel(model) {
224
+ return this.genAI.getGenerativeModel({ model });
225
+ }
213
226
  };
214
227
  AiService = __decorate([
215
228
  Singleton()
@@ -9,7 +9,7 @@ export type LogErrorOptions = {
9
9
  includeRest?: boolean;
10
10
  includeStack?: boolean;
11
11
  };
12
- /** either string as a module shorthand or object */
12
+ /** Either string as a module shorthand or object */
13
13
  export type LoggerArgument = string | undefined | {
14
14
  level?: LogLevel;
15
15
  module?: string | string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.92.3",
3
+ "version": "0.92.5",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"