@tstdl/base 0.93.62 → 0.93.66

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 (36) hide show
  1. package/ai/genkit/helpers.d.ts +10 -0
  2. package/ai/genkit/helpers.js +14 -0
  3. package/ai/genkit/index.d.ts +2 -0
  4. package/ai/genkit/index.js +2 -0
  5. package/ai/genkit/module.d.ts +35 -0
  6. package/ai/genkit/module.js +56 -0
  7. package/ai/index.d.ts +1 -0
  8. package/ai/index.js +1 -0
  9. package/ai/prompts/format.d.ts +15 -0
  10. package/ai/prompts/format.js +17 -0
  11. package/ai/prompts/index.d.ts +3 -0
  12. package/ai/prompts/index.js +3 -0
  13. package/ai/prompts/instructions-formatter.d.ts +25 -0
  14. package/ai/prompts/instructions-formatter.js +166 -0
  15. package/ai/prompts/instructions.d.ts +3 -0
  16. package/ai/prompts/instructions.js +8 -0
  17. package/document-management/server/services/document-file.service.d.ts +2 -0
  18. package/document-management/server/services/document-file.service.js +10 -9
  19. package/document-management/server/services/document-management-ai.service.d.ts +1 -0
  20. package/document-management/server/services/document-management-ai.service.js +267 -133
  21. package/document-management/server/services/document.service.js +1 -2
  22. package/examples/document-management/main.js +6 -0
  23. package/json-path/json-path.js +1 -1
  24. package/orm/server/repository.js +4 -6
  25. package/package.json +10 -6
  26. package/pdf/utils.js +1 -1
  27. package/schema/converters/zod-converter.d.ts +1 -1
  28. package/schema/converters/zod-converter.js +2 -13
  29. package/schema/converters/zod-v3-converter.d.ts +3 -3
  30. package/utils/date-time.d.ts +6 -0
  31. package/utils/date-time.js +36 -1
  32. package/utils/file-reader.d.ts +0 -1
  33. package/utils/file-reader.js +4 -7
  34. package/utils/object/object.d.ts +4 -2
  35. package/utils/object/object.js +30 -21
  36. package/utils/stream/from-promise.js +2 -2
@@ -58,9 +58,11 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
58
58
  });
59
59
  var _a;
60
60
  var DocumentManagementAiService_1;
61
+ import { readFile } from 'node:fs/promises';
61
62
  import { and, isNull as drizzleIsNull, eq, inArray } from 'drizzle-orm';
62
63
  import { P, match } from 'ts-pattern';
63
- import { AiService } from '../../../ai/index.js';
64
+ import { convertToGenkitSchema, genkitGenerationOptions, injectGenkit, injectModel } from '../../../ai/genkit/index.js';
65
+ import { formatData, formatInstructions, orderedList } from '../../../ai/prompts/index.js';
64
66
  import { TemporaryFile } from '../../../file/server/index.js';
65
67
  import { inject } from '../../../injector/inject.js';
66
68
  import { Logger } from '../../../logger/logger.js';
@@ -68,7 +70,7 @@ import { arrayAgg } from '../../../orm/index.js';
68
70
  import { injectRepository } from '../../../orm/server/index.js';
69
71
  import { array, boolean, enumeration, integer, nullable, number, object, string } from '../../../schema/index.js';
70
72
  import { distinct } from '../../../utils/array/index.js';
71
- import { numericDateToDateObject, tryDateObjectToNumericDate } from '../../../utils/date-time.js';
73
+ import { numericDateToDateTime, tryDateObjectToNumericDate } from '../../../utils/date-time.js';
72
74
  import { fromEntries, objectEntries } from '../../../utils/object/object.js';
73
75
  import { assertDefined, assertDefinedPass, assertNotNull, isNotNull, isNull, isUndefined } from '../../../utils/type-guards.js';
74
76
  import { Document, DocumentProperty, DocumentRequestState, DocumentTypeProperty } from '../../models/index.js';
@@ -79,78 +81,227 @@ import { DocumentFileService } from './document-file.service.js';
79
81
  import { DocumentPropertyService } from './document-property.service.js';
80
82
  import { DocumentTagService } from './document-tag.service.js';
81
83
  import { DocumentManagementSingleton } from './singleton.js';
82
- const CLASSIFY_MODEL = 'small';
83
- const EXTRACT_MODEL = 'medium';
84
- const ASSIGN_MODEL = 'small';
84
+ // --- Prompts ---
85
+ const ocrSystemPrompt = `
86
+ You are an expert OCR and Document Digitization engine.
87
+
88
+ ${formatInstructions({
89
+ 'Primary Objective': 'Convert the provided document into semantically structured, clean Markdown.',
90
+ 'Critical Constraints': orderedList([
91
+ 'Output ONLY the Markdown content. Do not include introductory text, conversational filler, or code block fences (```).',
92
+ 'Do not describe the visual appearance (e.g., "This looks like an invoice"). Transcribe the content only.',
93
+ ]),
94
+ 'Formatting Rules': orderedList({
95
+ 'Headings': 'Use # for the main document title (once). Use ##, ### for sections based on logical hierarchy.',
96
+ 'Text Content': 'Transcribe text verbatim. Do not correct spelling or grammar, summarize, or rewrite.',
97
+ 'Tables': 'Strictly use Markdown table syntax. Align columns logically based on the visual grid.',
98
+ 'Lists': 'Detect bullet points and numbered lists and format them as Markdown lists.',
99
+ 'Emphasis': 'Use **bold** and _italics_ only where visually distinct in the source.',
100
+ 'Columns': 'Read multi-column text as a single continuous flow.',
101
+ }),
102
+ 'Complex Elements': {
103
+ 'Images/Visuals': 'Replace non-textual diagrams with `> [Visual: Brief description of the image/chart]`.',
104
+ 'Signatures': 'Mark distinct signatures as `> [Signature: {Name if legible/Context}]`.',
105
+ 'Forms': 'Represent checkboxes as `[ ]` (unchecked) or `[x]` (checked). Format label/value pairs on separate lines or as a definition list if applicable.',
106
+ 'Math': 'Transcribe equations using LaTeX syntax enclosed in `$...$` for inline or `$$...$$` for block equations.',
107
+ },
108
+ 'Page Handling': [
109
+ 'Metadata: Start every page with `<!-- Page {n} Start -->` and end with `<!-- Page {n} End -->` on separate lines.',
110
+ 'Artifacts: Exclude running headers, footers, and page numbers unless they contain unique data not found elsewhere.',
111
+ ],
112
+ 'Error Handling': [
113
+ 'Mark illegible text as `[Illegible]`.',
114
+ 'Mark cut-off text as `[Cut off]`.',
115
+ ],
116
+ })}
117
+ `.trim();
118
+ const ocrUserPrompt = 'Transcribe the attached document into Markdown following the system instructions.';
119
+ const classifySystemPrompt = `
120
+ You are a Document Taxonomy Specialist.
121
+
122
+ ${formatInstructions({
123
+ 'Task': `Analyze the visual layout and text content of the document to categorize it into exactly one of the provided hierarchical types.`,
124
+ 'Input Context': 'You will be provided with a list of valid category labels (e.g., "Finance -> Invoice").',
125
+ 'Analysis Strategy': orderedList([
126
+ 'Scan the header and title for explicit document type names (e.g., "Invoice", "Contract", "Bill of Lading").',
127
+ 'Analyze the layout structure (e.g., columns often imply Invoices/Receipts; dense paragraphs imply Contracts/Letters).',
128
+ 'Identify key entities (e.g., "Total Due" implies financial; "Signed by" implies legal).',
129
+ ]),
130
+ 'Selection Logic': orderedList([
131
+ 'Exact Match: If the document explicitly states its type, select the corresponding category.',
132
+ 'Content Match: If implicit, match the intent.',
133
+ 'Specificity: Always choose the most specific leaf-node category available.',
134
+ 'Fallback: If ambiguous, choose the category that best describes the *primary* purpose of the document.',
135
+ ]),
136
+ })}
137
+ `.trim();
138
+ const classifyUserPrompt = 'Determine the single most accurate document type from the provided list based on the document following the system instructions.';
139
+ const extractSystemPrompt = `
140
+ You are a Structured Data Extraction Analyst.
141
+
142
+ ${formatInstructions({
143
+ 'Task': 'Analyze the document and extract metadata into the defined JSON schema.',
144
+ 'General Guidelines': orderedList({
145
+ 'Language': 'Ensure all generated text (titles, summaries) matches the primary language of the document.',
146
+ 'Null Handling': 'If a specific field or property is not present in the document, return null. Do not guess or hallucinate values.',
147
+ }),
148
+ 'Field Specific Instructions': {
149
+ 'Title': 'Create a concise, searchable filename-style title (e.g., "Invoice - Oct 2023").',
150
+ 'Subtitle': 'Extract context usually found below the header (e.g., Project Name, Reference Number).',
151
+ 'Summary': 'Write a 2-3 sentence executive summary. Mention the what type of information can be found in the document and its purpose.',
152
+ 'Tags': 'Generate 3-5 keywords for categorization. Only use important information missing in title, subtitle and properties. Prioritize reusing of existing tags where possible.',
153
+ 'Date': 'Identify the *creation* date of the document. If multiple dates exist, prioritize the primary date (like invoice or letter Date)',
154
+ },
155
+ 'Property Extraction': [
156
+ 'You will be given a list of specific dynamic properties to look for.',
157
+ 'Extract values *exactly* as they appear for strings.',
158
+ 'Normalize numbers and dates to standard formats.',
159
+ 'If a property is ambiguous, favor the value most prominent in the document layout.',
160
+ 'If a property is missing, set its value to null.',
161
+ ],
162
+ })}`.trim();
163
+ const extractUserPrompt = 'Analyze the document and extract metadata and specific properties defined in the output schema following the system instructions.';
164
+ const assignCollectionSystemPrompt = `
165
+ You are a Digital Filing Assistant.
166
+
167
+ ${formatInstructions({
168
+ 'Task': `Assign the document to relevant collections based on its metadata and content.`,
169
+ 'Input': 'Document Metadata and a list of Available Collections.',
170
+ 'Matching Logic': orderedList([
171
+ 'Direct Key-Match: Look for exact keyword matches between the collection name and the document metadata.',
172
+ 'Semantic Fit: Determine if the document functionally belongs to a group.',
173
+ 'Project Association: If the document references a specific project code or name found in a collection name, assign it there.',
174
+ ]),
175
+ 'Output': 'Return an array of matching collection IDs. If no collection is a strong fit, return an empty array.',
176
+ })}
177
+ `.trim();
178
+ const assignCollectionUserPrompt = 'Select the most appropriate collections for this document from the provided list following the system instructions.';
179
+ const assignRequestSystemPrompt = `
180
+ You are a Workflow Routing Agent.
181
+
182
+ ${formatInstructions({
183
+ 'Task': 'Match the provided document to an existing Open Document Request.',
184
+ 'Input': 'Document Metadata and a list of Open Requests.',
185
+ 'Matching Rules': orderedList({
186
+ 'Hard Constraints': 'If a Request has a "Comment" or specific property requirement, the document MUST fulfill it strictly (e.g., "Need bill from July" must match date).',
187
+ 'Ambiguity': 'If multiple requests match, select the one with the most specific constraints that are satisfied.',
188
+ 'Negative Match': 'If the document satisfies the metadata but violates a comment constraint, it is unsuitable.',
189
+ }),
190
+ 'Output': 'The ID of the matching request, or null if no request matches.',
191
+ })}
192
+ `.trim();
193
+ const assignRequestUserPrompt = 'Evaluate the document against the list of open requests and find the best match following the system instructions.';
85
194
  let DocumentManagementAiService = DocumentManagementAiService_1 = class DocumentManagementAiService {
195
+ #genkit = injectGenkit();
196
+ #ocrModel = injectModel('gemini-2.5-flash-lite').withConfig({ temperature: 0.25, topP: 0.75, topK: 8 });
197
+ #classifyModel = injectModel('gemini-2.5-flash').withConfig({ temperature: 0.25, topP: 0.75, topK: 8 });
198
+ #extractModel = injectModel('gemini-2.5-flash').withConfig({ temperature: 0.25, topP: 0.75, topK: 8 });
199
+ #assignModel = injectModel('gemini-2.5-flash').withConfig({ temperature: 0.25, topP: 0.75, topK: 8 });
86
200
  #documentCollectionService = inject(DocumentCollectionService);
87
201
  #documentTagService = inject(DocumentTagService);
88
202
  #documentCategoryTypeService = inject(DocumentCategoryTypeService);
89
203
  #documentFileService = inject(DocumentFileService);
90
204
  #documentPropertyService = inject(DocumentPropertyService);
91
- #aiService = inject(AiService);
92
205
  #documentPropertyRepository = injectRepository(DocumentProperty);
93
206
  #documentRepository = injectRepository(Document);
94
207
  #documentTypePropertyRepository = injectRepository(DocumentTypeProperty);
95
208
  #logger = inject(Logger, DocumentManagementAiService_1.name);
96
- async classifyDocumentType(tenantId, documentId) {
209
+ async extractDocumentContent(tenantId, documentId) {
97
210
  const env_1 = { stack: [], error: void 0, hasError: false };
211
+ try {
212
+ const document = await this.#documentRepository.loadByQuery({ tenantId, id: documentId });
213
+ const fileContentStream = this.#documentFileService.getContentStream(document);
214
+ const tmpFile = __addDisposableResource(env_1, await TemporaryFile.from(fileContentStream), true);
215
+ const buffer = await readFile(tmpFile.path);
216
+ const base64Data = buffer.toString('base64');
217
+ const dataUrl = `data:${document.mimeType};base64,${base64Data}`;
218
+ this.#logger.trace(`Extracting content from document ${document.id}`);
219
+ const result = await this.#genkit.generate({
220
+ model: this.#ocrModel,
221
+ output: { schema: convertToGenkitSchema(object({ content: string() })) },
222
+ system: ocrSystemPrompt,
223
+ prompt: [
224
+ { media: { url: dataUrl } },
225
+ { text: ocrUserPrompt },
226
+ ],
227
+ });
228
+ if (isNull(result.output)) {
229
+ throw new Error(`AI returned null output for document "${document.id}".`);
230
+ }
231
+ const markdownBlockStripped = result.output.content.trim().replaceAll(/^```\w*\s*|```$/gi, '').trim();
232
+ return markdownBlockStripped;
233
+ }
234
+ catch (e_1) {
235
+ env_1.error = e_1;
236
+ env_1.hasError = true;
237
+ }
238
+ finally {
239
+ const result_1 = __disposeResources(env_1);
240
+ if (result_1)
241
+ await result_1;
242
+ }
243
+ }
244
+ async classifyDocumentType(tenantId, documentId) {
245
+ const env_2 = { stack: [], error: void 0, hasError: false };
98
246
  try {
99
247
  const document = await this.#documentRepository.loadByQuery({ tenantId, id: documentId });
100
248
  if (isNotNull(document.typeId)) {
101
249
  return document.typeId;
102
250
  }
103
251
  const fileContentStream = this.#documentFileService.getContentStream(document);
104
- const tmpFile = __addDisposableResource(env_1, await TemporaryFile.from(fileContentStream), true);
105
- const filePart = await this.#aiService.processFile({ path: tmpFile.path, mimeType: document.mimeType });
252
+ const tmpFile = __addDisposableResource(env_2, await TemporaryFile.from(fileContentStream), true);
253
+ const buffer = await readFile(tmpFile.path);
254
+ const base64Data = buffer.toString('base64');
255
+ const dataUrl = `data:${document.mimeType};base64,${base64Data}`;
106
256
  const categories = await this.#documentCategoryTypeService.loadCategoryViews(tenantId);
107
257
  const typeLabelEntries = getDescriptiveTypeLabels(categories);
108
258
  const typeLabels = typeLabelEntries.map(({ label }) => label);
109
259
  this.#logger.trace(`Classifying document ${document.id}`);
110
- const documentTypeGeneration = await this.#aiService.generate({
111
- model: CLASSIFY_MODEL,
112
- generationOptions: {
260
+ const result = await this.#genkit.generate(genkitGenerationOptions({
261
+ model: this.#classifyModel,
262
+ config: {
113
263
  maxOutputTokens: 128,
114
- temperature: 0.1,
115
- topP: 0.75,
116
- topK: 4,
117
- thinkingBudget: 0,
264
+ thinkingConfig: { thinkingBudget: 0 },
118
265
  },
119
- generationSchema: object({
120
- documentType: enumeration(typeLabels),
121
- }),
122
- contents: [
123
- {
124
- role: 'user',
125
- parts: [
126
- { file: filePart.file },
127
- { text: `Klassifiziere den Inhalt des Dokuments in das angegebenen JSON Schema.` },
128
- ],
129
- },
266
+ output: {
267
+ schema: object({
268
+ documentType: enumeration(typeLabels),
269
+ }),
270
+ },
271
+ system: classifySystemPrompt,
272
+ prompt: [
273
+ { media: { url: dataUrl } },
274
+ { text: classifyUserPrompt },
130
275
  ],
131
- });
132
- const typeId = typeLabelEntries.find((entry) => entry.label == documentTypeGeneration.json.documentType)?.id;
276
+ }));
277
+ if (isNull(result.output)) {
278
+ throw new Error(`AI returned null output for document classification "${document.id}".`);
279
+ }
280
+ const output = result.output;
281
+ const typeId = typeLabelEntries.find((entry) => entry.label == output.documentType)?.id;
133
282
  assertDefined(typeId, `Could not classify document ${document.id}`);
134
283
  return typeId;
135
284
  }
136
- catch (e_1) {
137
- env_1.error = e_1;
138
- env_1.hasError = true;
285
+ catch (e_2) {
286
+ env_2.error = e_2;
287
+ env_2.hasError = true;
139
288
  }
140
289
  finally {
141
- const result_1 = __disposeResources(env_1);
142
- if (result_1)
143
- await result_1;
290
+ const result_2 = __disposeResources(env_2);
291
+ if (result_2)
292
+ await result_2;
144
293
  }
145
294
  }
146
295
  async extractDocumentInformation(tenantId, documentId) {
147
- const env_2 = { stack: [], error: void 0, hasError: false };
296
+ const env_3 = { stack: [], error: void 0, hasError: false };
148
297
  try {
149
298
  const document = await this.#documentRepository.loadByQuery({ tenantId, id: documentId });
150
299
  const existingTags = await this.#documentTagService.loadTags(tenantId);
151
300
  const fileContentStream = this.#documentFileService.getContentStream(document);
152
- const tmpFile = __addDisposableResource(env_2, await TemporaryFile.from(fileContentStream), true);
153
- const filePart = await this.#aiService.processFile({ path: tmpFile.path, mimeType: document.mimeType });
301
+ const tmpFile = __addDisposableResource(env_3, await TemporaryFile.from(fileContentStream), true);
302
+ const buffer = await readFile(tmpFile.path);
303
+ const base64Data = buffer.toString('base64');
304
+ const dataUrl = `data:${document.mimeType};base64,${base64Data}`;
154
305
  if (isNull(document.typeId)) {
155
306
  throw new Error(`Document ${document.id} has no type`);
156
307
  }
@@ -177,39 +328,31 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
177
328
  ? {}
178
329
  : { documentProperties: object(fromEntries(propertiesSchemaEntries)) }),
179
330
  });
180
- const context = { existingTags };
331
+ const tagLabels = existingTags.map((tag) => tag.label);
181
332
  this.#logger.trace(`Extracting document ${document.id}`);
182
- const { json: extraction } = await this.#aiService.generate({
183
- model: EXTRACT_MODEL,
184
- generationOptions: {
333
+ const result = await this.#genkit.generate(genkitGenerationOptions({
334
+ model: this.#extractModel,
335
+ output: { schema: generationSchema },
336
+ config: {
185
337
  maxOutputTokens: 2048,
186
- temperature: 0.2,
187
- topP: 0.5,
188
- topK: 16,
189
- thinkingBudget: 0,
338
+ thinkingConfig: { thinkingBudget: 0 },
190
339
  },
191
- generationSchema,
192
- contents: [
340
+ system: extractSystemPrompt,
341
+ prompt: [
342
+ { media: { url: dataUrl } },
193
343
  {
194
- role: 'user',
195
- parts: [
196
- { file: filePart.file },
197
- {
198
- text: `<context>
199
- ${JSON.stringify(context, null, 2)}
200
- </context>
201
- Extrahiere den Inhalt des Dokuments in das angegebenen JSON Schema.
344
+ text: `
345
+ ${formatData({ existingTags: tagLabels })}
202
346
 
203
- Vermeide es, den Titel im Untertitel zu wiederholen.
204
- Gib in der summary ausführlich an, welche Informationen in dem Dokument vorkommen (ohne konkrete Werte).
205
- Erstelle bis zu 5 Tags. Verwende vorhandene Tags, wenn sie passen. Erstelle neue Tags, wenn es keine passenden gibt.
206
- Vermeide es, den Titel oder Untertitel als Tag zu verwenden.
207
- Antworte auf deutsch.`,
208
- },
209
- ],
347
+ ${extractUserPrompt}
348
+ `.trim(),
210
349
  },
211
350
  ],
212
- });
351
+ }));
352
+ if (isNull(result.output)) {
353
+ throw new Error(`AI returned null output for document extraction "${document.id}".`);
354
+ }
355
+ const extraction = result.output;
213
356
  const filteredDocumentTags = extraction.documentTags.filter((tag) => (tag != extraction.documentTitle) && (tag != extraction.documentSubtitle));
214
357
  const date = isNotNull(extraction.documentDate) ? tryAiOutputDateObjectToNumericDate(extraction.documentDate) : null;
215
358
  const parsedProperties = isUndefined(extraction.documentProperties)
@@ -238,14 +381,14 @@ Antworte auf deutsch.`,
238
381
  properties: parsedProperties,
239
382
  };
240
383
  }
241
- catch (e_2) {
242
- env_2.error = e_2;
243
- env_2.hasError = true;
384
+ catch (e_3) {
385
+ env_3.error = e_3;
386
+ env_3.hasError = true;
244
387
  }
245
388
  finally {
246
- const result_2 = __disposeResources(env_2);
247
- if (result_2)
248
- await result_2;
389
+ const result_3 = __disposeResources(env_3);
390
+ if (result_3)
391
+ await result_3;
249
392
  }
250
393
  }
251
394
  async findSuitableCollectionsForDocument(document, collectionIds) {
@@ -261,41 +404,36 @@ Antworte auf deutsch.`,
261
404
  }));
262
405
  const documentTagLabels = documentTags.map((tag) => tag.label);
263
406
  const propertyEntries = documentProperties.map((property) => [property.label, property.value]);
264
- const context = {
265
- document: {
266
- title: document.title ?? undefined,
267
- subtitle: document.subtitle ?? undefined,
268
- date: isNotNull(document.date) ? numericDateToDateObject(document.date) : undefined,
269
- summary: document.summary ?? undefined,
270
- tags: (documentTagLabels.length > 0) ? documentTagLabels : undefined,
271
- properties: fromEntries(propertyEntries),
272
- },
273
- collections,
407
+ const documentData = {
408
+ title: document.title ?? null,
409
+ subtitle: document.subtitle ?? null,
410
+ date: isNotNull(document.date) ? numericDateToDateTime(document.date).toISODate() : null,
411
+ tags: (documentTagLabels.length > 0) ? documentTagLabels : null,
412
+ summary: document.summary ?? null,
274
413
  };
275
- const result = await this.#aiService.generate({
276
- model: ASSIGN_MODEL,
277
- generationOptions: {
278
- maxOutputTokens: 100,
279
- temperature: 0,
280
- topP: 0.2,
281
- topK: 16,
282
- thinkingBudget: 0,
414
+ const result = await this.#genkit.generate(genkitGenerationOptions({
415
+ model: this.#assignModel,
416
+ output: { schema: object({ collectionIds: array(string()) }) },
417
+ config: {
418
+ maxOutputTokens: 512,
419
+ thinkingConfig: {
420
+ thinkingBudget: 0,
421
+ },
283
422
  },
284
- generationSchema: object({ collectionIds: array(string()) }),
285
- contents: [{
286
- role: 'user',
287
- parts: [
288
- {
289
- text: `<context>
290
- ${JSON.stringify(context, null, 2)}
291
- </context>
423
+ system: assignCollectionSystemPrompt,
424
+ prompt: [{
425
+ text: `
426
+ ${formatData({ document: documentData, documentProperties: fromEntries(propertyEntries) })}
427
+
428
+ ${formatData({ collections })}
292
429
 
293
- Ordne das Dokument unter "document" einer oder mehreren passenden Collection unter "collections" zu. Gib es als JSON im angegebenen Schema aus. Wenn keine Collection passt, gib collectionIds als leeres Array zurück.`,
294
- },
295
- ],
430
+ ${assignCollectionUserPrompt}`,
296
431
  }],
297
- });
298
- return result.json.collectionIds;
432
+ }));
433
+ if (isNull(result.output)) {
434
+ throw new Error(`AI returned null output for collection assignment "${document.id}".`);
435
+ }
436
+ return result.output.collectionIds;
299
437
  }
300
438
  async findSuitableRequestForDocument(document, collectionIds) {
301
439
  const session = this.#documentPropertyRepository.session;
@@ -322,44 +460,40 @@ Ordne das Dokument unter "document" einer oder mehreren passenden Collection unt
322
460
  const requests = openRequestsWithoutDocument.map((request) => ({
323
461
  id: request.id,
324
462
  collections: request.collectionIds.map((collectionId) => assertDefinedPass(collectionNamesMap[collectionId]).name),
325
- comment: request.comment ?? undefined,
463
+ comment: request.comment ?? null,
326
464
  }));
327
465
  const propertyEntries = documentProperties.map((property) => [property.label, property.value]);
328
- const context = {
329
- document: {
330
- title: document.title ?? undefined,
331
- subtitle: document.subtitle ?? undefined,
332
- date: isNotNull(document.date) ? numericDateToDateObject(document.date) : undefined,
333
- summary: document.summary ?? undefined,
334
- tags: (documentTagLabels.length > 0) ? documentTagLabels : undefined,
335
- properties: fromEntries(propertyEntries),
336
- },
337
- requests,
466
+ const documentData = {
467
+ title: document.title ?? null,
468
+ subtitle: document.subtitle ?? null,
469
+ date: isNotNull(document.date) ? numericDateToDateTime(document.date).toISODate() : null,
470
+ tags: (documentTagLabels.length > 0) ? documentTagLabels : null,
471
+ summary: document.summary ?? null,
338
472
  };
339
- const result = await this.#aiService.generate({
340
- model: ASSIGN_MODEL,
341
- generationOptions: {
342
- maxOutputTokens: 100,
343
- temperature: 0,
344
- topP: 0.2,
345
- topK: 16,
346
- thinkingBudget: 0,
473
+ const result = await this.#genkit.generate(genkitGenerationOptions({
474
+ model: this.#assignModel,
475
+ config: {
476
+ maxOutputTokens: 128,
477
+ thinkingConfig: {
478
+ thinkingBudget: 0,
479
+ },
347
480
  },
348
- generationSchema: object({ requestId: nullable(string()) }),
349
- contents: [{
350
- role: 'user',
351
- parts: [
352
- {
353
- text: `<context>
354
- ${JSON.stringify(context, null, 2)}
355
- </context>
481
+ output: { schema: object({ requestId: nullable(string()) }) },
482
+ system: assignRequestSystemPrompt,
483
+ prompt: [{
484
+ text: `
485
+ ${formatData({ document: documentData, documentProperties: fromEntries(propertyEntries) })}
486
+
487
+ ${formatData({ requests })}
356
488
 
357
- Ordne das Dokument unter "document" der passenden Anforderungen unter "requests" zu. Gib es als JSON im angegebenen Schema aus. Wenn keine Anforderung passt, setze requestId auf null.`,
358
- },
359
- ],
489
+ ${assignRequestUserPrompt}
490
+ `.trim(),
360
491
  }],
361
- });
362
- return result.json.requestId;
492
+ }));
493
+ if (isNull(result.output)) {
494
+ throw new Error(`AI returned null output for request assignment "${document.id}".`);
495
+ }
496
+ return result.output.requestId;
363
497
  }
364
498
  };
365
499
  DocumentManagementAiService = DocumentManagementAiService_1 = __decorate([
@@ -17,7 +17,7 @@ import { objectKeys } from '../../../utils/object/object.js';
17
17
  import { readableStreamFromPromise } from '../../../utils/stream/from-promise.js';
18
18
  import { tryIgnoreLogAsync } from '../../../utils/try-ignore.js';
19
19
  import { isDefined, isNotReadableStream, isNotUint8Array, isUndefined } from '../../../utils/type-guards.js';
20
- import { Document, DocumentApproval, DocumentAssignmentScope, DocumentAssignmentTask, DocumentType, DocumentWorkflowStep } from '../../models/index.js';
20
+ import { Document, DocumentApproval, DocumentAssignmentScope, DocumentAssignmentTask, DocumentWorkflowStep } from '../../models/index.js';
21
21
  import { DocumentCollectionService } from './document-collection.service.js';
22
22
  import { DocumentFileService } from './document-file.service.js';
23
23
  import { DocumentManagementObservationService } from './document-management-observation.service.js';
@@ -33,7 +33,6 @@ let DocumentService = DocumentService_1 = class DocumentService extends Transact
33
33
  #workflowService = injectTransactional(DocumentWorkflowService);
34
34
  #documentPropertyService = injectTransactional(DocumentPropertyService);
35
35
  #collectionService = injectTransactional(DocumentCollectionService);
36
- #documentTypeRepository = injectRepository(DocumentType);
37
36
  #documentAssignmentTaskRepository = injectRepository(DocumentAssignmentTask);
38
37
  #documentAssignmentScopeRepository = injectRepository(DocumentAssignmentScope);
39
38
  #observationService = inject(DocumentManagementObservationService);
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import '../../polyfills.js';
8
+ import { configureGenkit } from '../../ai/genkit/module.js';
8
9
  import { configureAiService } from '../../ai/index.js';
9
10
  import { MockApiRequestTokenProvider } from '../../api/server/api-request-token.provider.js';
10
11
  import { configureApiServer } from '../../api/server/module.js';
@@ -88,6 +89,11 @@ async function bootstrap() {
88
89
  configurePostgresQueue();
89
90
  configureLocalMessageBus();
90
91
  configureDefaultSignalsImplementation();
92
+ configureGenkit({
93
+ gemini: {
94
+ apiKey: config.ai.apiKey,
95
+ },
96
+ });
91
97
  configureOrm({
92
98
  connection: {
93
99
  host: config.database.host,
@@ -1,7 +1,7 @@
1
1
  import { isIterable } from '../utils/iterable-helpers/is-iterable.js';
2
2
  import { assertDefinedPass, isArray, isDefined, isString, isSymbol, isUndefined } from '../utils/type-guards.js';
3
3
  const numberPattern = /^\d+$/u;
4
- const parsePattern = /(?:(?:^|\.)(?<dot>[^.[]+))|(?<root>^\$)|\[(?:(?:'(?<bracket>.+?)')|(?<index>\d+)|(?:Symbol\((?<symbol>.*)\)))\]|(?<error>.+?)/ug;
4
+ const parsePattern = /(?:(?:^|\.)(?<dot>[^.[]+))|(?<root>^\$)|\[(?:(?:['"](?<bracket>.+?)['"])|(?<index>\d+)|(?:Symbol\((?<symbol>.*)\)))\]|(?<error>.+?)/ug;
5
5
  export class JsonPath {
6
6
  _options;
7
7
  _path;
@@ -11,12 +11,11 @@ import { NotFoundError } from '../../errors/not-found.error.js';
11
11
  import { Singleton } from '../../injector/decorators.js';
12
12
  import { inject, injectArgument } from '../../injector/inject.js';
13
13
  import { afterResolve, resolveArgumentType } from '../../injector/interfaces.js';
14
- import { Schema } from '../../schema/schema.js';
15
14
  import { distinct, toArray } from '../../utils/array/array.js';
16
15
  import { mapAsync } from '../../utils/async-iterable-helpers/map.js';
17
16
  import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
18
17
  import { importSymmetricKey } from '../../utils/cryptography.js';
19
- import { fromDeepObjectEntries, fromEntries, objectEntries } from '../../utils/object/object.js';
18
+ import { assignDeep, fromEntries, objectEntries } from '../../utils/object/object.js';
20
19
  import { toSnakeCase } from '../../utils/string/index.js';
21
20
  import { cancelableTimeout } from '../../utils/timing.js';
22
21
  import { tryIgnoreAsync } from '../../utils/try-ignore.js';
@@ -1073,14 +1072,13 @@ let EntityRepository = class EntityRepository extends Transactional {
1073
1072
  return await toArrayAsync(mapAsync(columns, async (column) => await this._mapToEntity(column, transformContext)));
1074
1073
  }
1075
1074
  async _mapToEntity(columns, transformContext) {
1076
- const entries = [];
1075
+ const entity = new this.type();
1077
1076
  for (const def of this.#columnDefinitions) {
1078
1077
  const rawValue = columns[def.name];
1079
1078
  const transformed = await def.fromDatabase(rawValue, transformContext);
1080
- entries.push([def.objectPath, transformed]); // eslint-disable-line @typescript-eslint/no-unsafe-argument
1079
+ assignDeep(entity, def.objectPath, transformed);
1081
1080
  }
1082
- const obj = fromDeepObjectEntries(entries);
1083
- return Schema.parse(this.type, obj);
1081
+ return entity;
1084
1082
  }
1085
1083
  async _mapManyToColumns(objects, transformContext) {
1086
1084
  return await toArrayAsync(mapAsync(objects, async (obj) => await this._mapToColumns(obj, transformContext)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.62",
3
+ "version": "0.93.66",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,6 +37,7 @@
37
37
  "./supports": "./supports.js",
38
38
  "./tokens": "./tokens.js",
39
39
  "./ai": "./ai/index.js",
40
+ "./ai/genkit": "./ai/genkit/index.js",
40
41
  "./api": "./api/index.js",
41
42
  "./api/client": "./api/client/index.js",
42
43
  "./api/server": "./api/server/index.js",
@@ -138,18 +139,21 @@
138
139
  "type-fest": "^5.3"
139
140
  },
140
141
  "peerDependencies": {
142
+ "@genkit-ai/google-genai": "^1.25",
141
143
  "@google-cloud/storage": "^7.18",
142
- "@google/genai": "^1.31",
144
+ "@google/genai": "^1.32",
145
+ "@toon-format/toon": "^2.1.0",
143
146
  "@tstdl/angular": "^0.93",
144
147
  "@zxcvbn-ts/core": "^3.0",
145
148
  "@zxcvbn-ts/language-common": "^3.0",
146
149
  "@zxcvbn-ts/language-de": "^3.0",
147
150
  "@zxcvbn-ts/language-en": "^3.0",
148
- "drizzle-orm": "^0.44",
151
+ "drizzle-orm": "^0.45",
149
152
  "file-type": "^21.1",
153
+ "genkit": "^1.25",
150
154
  "handlebars": "^4.7",
151
155
  "minio": "^8.0",
152
- "mjml": "^4.17",
156
+ "mjml": "^4.18",
153
157
  "nodemailer": "^7.0",
154
158
  "pg": "^8.16",
155
159
  "playwright": "^1.57",
@@ -158,7 +162,7 @@
158
162
  "sharp": "^0.34",
159
163
  "undici": "^7.16",
160
164
  "urlpattern-polyfill": "^10.1",
161
- "zod": "^4.1"
165
+ "zod": "^3.25"
162
166
  },
163
167
  "peerDependenciesMeta": {
164
168
  "@tstdl/angular": {
@@ -182,7 +186,7 @@
182
186
  "typedoc-plugin-markdown": "4.9",
183
187
  "typedoc-plugin-missing-exports": "4.1",
184
188
  "typescript": "5.9",
185
- "typescript-eslint": "8.48"
189
+ "typescript-eslint": "8.49"
186
190
  },
187
191
  "overrides": {
188
192
  "drizzle-kit": {