@tstdl/base 0.92.2 → 0.92.4

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
- import { SchemaTestable } from '../schema/index.js';
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
- getFiles(files: FileInput[]): Promise<FileMetadataResponse[]>;
26
- classify<T extends EnumerationType>(fileInput: FileInput, types: T): Promise<{
27
- reasoning: string;
42
+ getFiles(files: readonly FileInput[]): Promise<FileMetadataResponse[]>;
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: 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,10 +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';
72
+ import { toArray } from '../utils/array/array.js';
71
73
  import { digest } from '../utils/cryptography.js';
74
+ import { formatBytes } from '../utils/format.js';
72
75
  import { timeout } from '../utils/timing.js';
73
76
  import { tryIgnoreAsync } from '../utils/try-ignore.js';
74
77
  import { isBlob } from '../utils/type-guards.js';
@@ -76,19 +79,21 @@ import { millisecondsPerSecond } from '../utils/units.js';
76
79
  let AiService = class AiService {
77
80
  #options = injectArgument(this);
78
81
  #fileCache = new Map();
82
+ #logger = inject(Logger, 'AiService');
79
83
  genAI = new GoogleGenerativeAI(this.#options.apiKey);
80
84
  fileManager = new GoogleAIFileManager(this.#options.apiKey);
81
- model = this.genAI.getGenerativeModel({ model: this.#options.model ?? 'gemini-2.0-flash-exp' });
85
+ defaultModel = this.#options.defaultModel ?? 'gemini-2.0-flash-exp';
82
86
  async getFile(fileInput) {
83
87
  const path = isBlob(fileInput) ? join(tmpdir(), crypto.randomUUID()) : fileInput.path;
84
88
  const mimeType = isBlob(fileInput) ? fileInput.type : fileInput.mimeType;
85
89
  const blob = isBlob(fileInput) ? fileInput : await openAsBlob(path, { type: mimeType });
86
90
  const buffer = await blob.arrayBuffer();
87
91
  const byteArray = new Uint8Array(buffer);
88
- const fileHash = await digest('SHA-256', byteArray).toBase64();
92
+ const fileHash = await digest('SHA-1', byteArray).toBase64();
89
93
  const fileKey = `${fileHash}:${byteArray.length}`;
90
94
  if (this.#fileCache.has(fileKey)) {
91
95
  try {
96
+ this.#logger.verbose(`Fetching file "${fileHash}" from cache...`);
92
97
  const cachedFile = await this.#fileCache.get(fileKey);
93
98
  return await this.fileManager.getFile(cachedFile.name);
94
99
  }
@@ -105,7 +110,9 @@ let AiService = class AiService {
105
110
  stack.defer(async () => tryIgnoreAsync(async () => unlink(path)));
106
111
  await writeFile(path, byteArray);
107
112
  }
113
+ this.#logger.verbose(`Uploading file "${fileHash}" (${formatBytes(byteArray.length)})...`);
108
114
  const result = await this.fileManager.uploadFile(path, { mimeType });
115
+ this.#logger.verbose(`Processing file "${fileHash}"...`);
109
116
  return await this.waitForFileActive(result.file);
110
117
  }
111
118
  catch (e_1) {
@@ -129,20 +136,21 @@ let AiService = class AiService {
129
136
  async getFiles(files) {
130
137
  return Promise.all(files.map(async (file) => this.getFile(file)));
131
138
  }
132
- async classify(fileInput, types) {
133
- const file = await this.getFile(fileInput);
139
+ async classify(fileInput, types, options) {
140
+ const files = await this.getFiles(toArray(fileInput));
134
141
  const resultSchema = object({
135
- reasoning: string({ description: 'Reasoning for classification. Use to be more confident, if unsure. Reason for every somewhat likely document type.' }),
136
142
  types: nullable(array(object({
137
143
  type: enumeration(types, { description: 'Type of document' }),
138
144
  confidence: enumeration(['high', 'medium', 'low'], { description: 'How sure/certain you are about the classficiation.' })
139
145
  }), { description: 'One or more document types that matches' }))
140
146
  });
141
147
  const responseSchema = convertToOpenApiSchema(resultSchema);
142
- const result = await this.model.generateContent({
148
+ this.#logger.verbose('Classifying...');
149
+ const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
143
150
  generationConfig: {
144
- maxOutputTokens: 1024,
145
- temperature: 0.5,
151
+ maxOutputTokens: 2048,
152
+ temperature: 0.75,
153
+ ...options,
146
154
  responseMimeType: 'application/json',
147
155
  responseSchema
148
156
  },
@@ -151,18 +159,22 @@ let AiService = class AiService {
151
159
  {
152
160
  role: 'user',
153
161
  parts: [
154
- { fileData: { mimeType: file.mimeType, fileUri: file.uri } },
162
+ ...files.map((file) => ({ fileData: { mimeType: file.mimeType, fileUri: file.uri } })),
155
163
  { text: `Classify the document. Output as JSON using the following schema:\n${JSON.stringify(responseSchema, null, 2)}\n\nIf none of the provided document types are a suitable match, return null for types.` }
156
164
  ]
157
165
  }
158
166
  ]
159
167
  });
160
- 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
+ };
161
172
  }
162
- async extractData(fileInput, schema) {
163
- const file = await this.getFile(fileInput);
173
+ async extractData(fileInput, schema, options) {
174
+ const files = await this.getFiles(toArray(fileInput));
164
175
  const responseSchema = convertToOpenApiSchema(schema);
165
- const result = await this.model.generateContent({
176
+ this.#logger.verbose('Extracting data...');
177
+ const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
166
178
  generationConfig: {
167
179
  maxOutputTokens: 4096,
168
180
  temperature: 0.5,
@@ -172,23 +184,21 @@ let AiService = class AiService {
172
184
  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.
173
185
 
174
186
  **Instructions:**
175
- 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.
176
-
177
- **Reasoning**
178
- 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.
179
-
180
- You *MUST* output the reasoning first.`,
187
+ 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.`,
181
188
  contents: [
182
189
  {
183
190
  role: 'user',
184
191
  parts: [
185
- { fileData: { mimeType: file.mimeType, fileUri: file.uri } },
192
+ ...files.map((file) => ({ fileData: { mimeType: file.mimeType, fileUri: file.uri } })),
186
193
  { text: `Classify the document. Output as JSON using the following schema:\n${JSON.stringify(responseSchema, null, 2)}` }
187
194
  ]
188
195
  }
189
196
  ]
190
197
  });
191
- return Schema.parse(schema, JSON.parse(result.response.text()));
198
+ return {
199
+ usage: result.response.usageMetadata,
200
+ result: Schema.parse(schema, JSON.parse(result.response.text()))
201
+ };
192
202
  }
193
203
  async waitForFileActive(fileMetadata) {
194
204
  let file = await this.fileManager.getFile(fileMetadata.name);
@@ -209,6 +219,9 @@ You *MUST* output the reasoning first.`,
209
219
  }
210
220
  return responses;
211
221
  }
222
+ getModel(model) {
223
+ return this.genAI.getGenerativeModel({ model });
224
+ }
212
225
  };
213
226
  AiService = __decorate([
214
227
  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.2",
3
+ "version": "0.92.4",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"