@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.
- package/ai/ai.service.d.ts +26 -9
- package/ai/ai.service.js +34 -21
- 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
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
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
|
|
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,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
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
148
|
+
this.#logger.verbose('Classifying...');
|
|
149
|
+
const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
|
|
144
150
|
generationConfig: {
|
|
145
|
-
maxOutputTokens:
|
|
146
|
-
temperature: 0.
|
|
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
|
|
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
|
-
|
|
176
|
+
this.#logger.verbose('Extracting data...');
|
|
177
|
+
const result = await this.getModel(options?.model ?? this.defaultModel).generateContent({
|
|
167
178
|
generationConfig: {
|
|
168
|
-
maxOutputTokens:
|
|
169
|
-
temperature: 0.
|
|
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
|
|
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()
|
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[];
|