@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.
- package/ai/ai.service.d.ts +28 -11
- package/ai/ai.service.js +36 -23
- package/logger/logger.d.ts +1 -1
- package/package.json +1 -1
package/ai/ai.service.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
34
|
-
waitForFileActive
|
|
35
|
-
waitForFilesActive
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
148
|
+
this.#logger.verbose('Classifying...');
|
|
149
|
+
const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
|
|
143
150
|
generationConfig: {
|
|
144
|
-
maxOutputTokens:
|
|
145
|
-
temperature: 0.
|
|
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
|
|
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
|
|
173
|
+
async extractData(fileInput, schema, options) {
|
|
174
|
+
const files = await this.getFiles(toArray(fileInput));
|
|
164
175
|
const responseSchema = convertToOpenApiSchema(schema);
|
|
165
|
-
|
|
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
|
|
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()
|
package/logger/logger.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type LogErrorOptions = {
|
|
|
9
9
|
includeRest?: boolean;
|
|
10
10
|
includeStack?: boolean;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
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[];
|