@tstdl/base 0.93.62 → 0.93.64

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 (34) 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 +266 -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/file-reader.d.ts +0 -1
  31. package/utils/file-reader.js +4 -7
  32. package/utils/object/object.d.ts +4 -2
  33. package/utils/object/object.js +30 -21
  34. 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,226 @@ 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
+ return result.output.content;
232
+ }
233
+ catch (e_1) {
234
+ env_1.error = e_1;
235
+ env_1.hasError = true;
236
+ }
237
+ finally {
238
+ const result_1 = __disposeResources(env_1);
239
+ if (result_1)
240
+ await result_1;
241
+ }
242
+ }
243
+ async classifyDocumentType(tenantId, documentId) {
244
+ const env_2 = { stack: [], error: void 0, hasError: false };
98
245
  try {
99
246
  const document = await this.#documentRepository.loadByQuery({ tenantId, id: documentId });
100
247
  if (isNotNull(document.typeId)) {
101
248
  return document.typeId;
102
249
  }
103
250
  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 });
251
+ const tmpFile = __addDisposableResource(env_2, await TemporaryFile.from(fileContentStream), true);
252
+ const buffer = await readFile(tmpFile.path);
253
+ const base64Data = buffer.toString('base64');
254
+ const dataUrl = `data:${document.mimeType};base64,${base64Data}`;
106
255
  const categories = await this.#documentCategoryTypeService.loadCategoryViews(tenantId);
107
256
  const typeLabelEntries = getDescriptiveTypeLabels(categories);
108
257
  const typeLabels = typeLabelEntries.map(({ label }) => label);
109
258
  this.#logger.trace(`Classifying document ${document.id}`);
110
- const documentTypeGeneration = await this.#aiService.generate({
111
- model: CLASSIFY_MODEL,
112
- generationOptions: {
259
+ const result = await this.#genkit.generate(genkitGenerationOptions({
260
+ model: this.#classifyModel,
261
+ config: {
113
262
  maxOutputTokens: 128,
114
- temperature: 0.1,
115
- topP: 0.75,
116
- topK: 4,
117
- thinkingBudget: 0,
263
+ thinkingConfig: { thinkingBudget: 0 },
118
264
  },
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
- },
265
+ output: {
266
+ schema: object({
267
+ documentType: enumeration(typeLabels),
268
+ }),
269
+ },
270
+ system: classifySystemPrompt,
271
+ prompt: [
272
+ { media: { url: dataUrl } },
273
+ { text: classifyUserPrompt },
130
274
  ],
131
- });
132
- const typeId = typeLabelEntries.find((entry) => entry.label == documentTypeGeneration.json.documentType)?.id;
275
+ }));
276
+ if (isNull(result.output)) {
277
+ throw new Error(`AI returned null output for document classification "${document.id}".`);
278
+ }
279
+ const output = result.output;
280
+ const typeId = typeLabelEntries.find((entry) => entry.label == output.documentType)?.id;
133
281
  assertDefined(typeId, `Could not classify document ${document.id}`);
134
282
  return typeId;
135
283
  }
136
- catch (e_1) {
137
- env_1.error = e_1;
138
- env_1.hasError = true;
284
+ catch (e_2) {
285
+ env_2.error = e_2;
286
+ env_2.hasError = true;
139
287
  }
140
288
  finally {
141
- const result_1 = __disposeResources(env_1);
142
- if (result_1)
143
- await result_1;
289
+ const result_2 = __disposeResources(env_2);
290
+ if (result_2)
291
+ await result_2;
144
292
  }
145
293
  }
146
294
  async extractDocumentInformation(tenantId, documentId) {
147
- const env_2 = { stack: [], error: void 0, hasError: false };
295
+ const env_3 = { stack: [], error: void 0, hasError: false };
148
296
  try {
149
297
  const document = await this.#documentRepository.loadByQuery({ tenantId, id: documentId });
150
298
  const existingTags = await this.#documentTagService.loadTags(tenantId);
151
299
  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 });
300
+ const tmpFile = __addDisposableResource(env_3, await TemporaryFile.from(fileContentStream), true);
301
+ const buffer = await readFile(tmpFile.path);
302
+ const base64Data = buffer.toString('base64');
303
+ const dataUrl = `data:${document.mimeType};base64,${base64Data}`;
154
304
  if (isNull(document.typeId)) {
155
305
  throw new Error(`Document ${document.id} has no type`);
156
306
  }
@@ -177,39 +327,31 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
177
327
  ? {}
178
328
  : { documentProperties: object(fromEntries(propertiesSchemaEntries)) }),
179
329
  });
180
- const context = { existingTags };
330
+ const tagLabels = existingTags.map((tag) => tag.label);
181
331
  this.#logger.trace(`Extracting document ${document.id}`);
182
- const { json: extraction } = await this.#aiService.generate({
183
- model: EXTRACT_MODEL,
184
- generationOptions: {
332
+ const result = await this.#genkit.generate(genkitGenerationOptions({
333
+ model: this.#extractModel,
334
+ output: { schema: generationSchema },
335
+ config: {
185
336
  maxOutputTokens: 2048,
186
- temperature: 0.2,
187
- topP: 0.5,
188
- topK: 16,
189
- thinkingBudget: 0,
337
+ thinkingConfig: { thinkingBudget: 0 },
190
338
  },
191
- generationSchema,
192
- contents: [
339
+ system: extractSystemPrompt,
340
+ prompt: [
341
+ { media: { url: dataUrl } },
193
342
  {
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.
343
+ text: `
344
+ ${formatData({ existingTags: tagLabels })}
202
345
 
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
- ],
346
+ ${extractUserPrompt}
347
+ `.trim(),
210
348
  },
211
349
  ],
212
- });
350
+ }));
351
+ if (isNull(result.output)) {
352
+ throw new Error(`AI returned null output for document extraction "${document.id}".`);
353
+ }
354
+ const extraction = result.output;
213
355
  const filteredDocumentTags = extraction.documentTags.filter((tag) => (tag != extraction.documentTitle) && (tag != extraction.documentSubtitle));
214
356
  const date = isNotNull(extraction.documentDate) ? tryAiOutputDateObjectToNumericDate(extraction.documentDate) : null;
215
357
  const parsedProperties = isUndefined(extraction.documentProperties)
@@ -238,14 +380,14 @@ Antworte auf deutsch.`,
238
380
  properties: parsedProperties,
239
381
  };
240
382
  }
241
- catch (e_2) {
242
- env_2.error = e_2;
243
- env_2.hasError = true;
383
+ catch (e_3) {
384
+ env_3.error = e_3;
385
+ env_3.hasError = true;
244
386
  }
245
387
  finally {
246
- const result_2 = __disposeResources(env_2);
247
- if (result_2)
248
- await result_2;
388
+ const result_3 = __disposeResources(env_3);
389
+ if (result_3)
390
+ await result_3;
249
391
  }
250
392
  }
251
393
  async findSuitableCollectionsForDocument(document, collectionIds) {
@@ -261,41 +403,36 @@ Antworte auf deutsch.`,
261
403
  }));
262
404
  const documentTagLabels = documentTags.map((tag) => tag.label);
263
405
  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,
406
+ const documentData = {
407
+ title: document.title ?? null,
408
+ subtitle: document.subtitle ?? null,
409
+ date: isNotNull(document.date) ? numericDateToDateTime(document.date).toISODate() : null,
410
+ tags: (documentTagLabels.length > 0) ? documentTagLabels : null,
411
+ summary: document.summary ?? null,
274
412
  };
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,
413
+ const result = await this.#genkit.generate(genkitGenerationOptions({
414
+ model: this.#assignModel,
415
+ output: { schema: object({ collectionIds: array(string()) }) },
416
+ config: {
417
+ maxOutputTokens: 512,
418
+ thinkingConfig: {
419
+ thinkingBudget: 0,
420
+ },
283
421
  },
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>
422
+ system: assignCollectionSystemPrompt,
423
+ prompt: [{
424
+ text: `
425
+ ${formatData({ document: documentData, documentProperties: fromEntries(propertyEntries) })}
426
+
427
+ ${formatData({ collections })}
292
428
 
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
- ],
429
+ ${assignCollectionUserPrompt}`,
296
430
  }],
297
- });
298
- return result.json.collectionIds;
431
+ }));
432
+ if (isNull(result.output)) {
433
+ throw new Error(`AI returned null output for collection assignment "${document.id}".`);
434
+ }
435
+ return result.output.collectionIds;
299
436
  }
300
437
  async findSuitableRequestForDocument(document, collectionIds) {
301
438
  const session = this.#documentPropertyRepository.session;
@@ -322,44 +459,40 @@ Ordne das Dokument unter "document" einer oder mehreren passenden Collection unt
322
459
  const requests = openRequestsWithoutDocument.map((request) => ({
323
460
  id: request.id,
324
461
  collections: request.collectionIds.map((collectionId) => assertDefinedPass(collectionNamesMap[collectionId]).name),
325
- comment: request.comment ?? undefined,
462
+ comment: request.comment ?? null,
326
463
  }));
327
464
  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,
465
+ const documentData = {
466
+ title: document.title ?? null,
467
+ subtitle: document.subtitle ?? null,
468
+ date: isNotNull(document.date) ? numericDateToDateTime(document.date).toISODate() : null,
469
+ tags: (documentTagLabels.length > 0) ? documentTagLabels : null,
470
+ summary: document.summary ?? null,
338
471
  };
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,
472
+ const result = await this.#genkit.generate(genkitGenerationOptions({
473
+ model: this.#assignModel,
474
+ config: {
475
+ maxOutputTokens: 128,
476
+ thinkingConfig: {
477
+ thinkingBudget: 0,
478
+ },
347
479
  },
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>
480
+ output: { schema: object({ requestId: nullable(string()) }) },
481
+ system: assignRequestSystemPrompt,
482
+ prompt: [{
483
+ text: `
484
+ ${formatData({ document: documentData, documentProperties: fromEntries(propertyEntries) })}
485
+
486
+ ${formatData({ requests })}
356
487
 
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
- ],
488
+ ${assignRequestUserPrompt}
489
+ `.trim(),
360
490
  }],
361
- });
362
- return result.json.requestId;
491
+ }));
492
+ if (isNull(result.output)) {
493
+ throw new Error(`AI returned null output for request assignment "${document.id}".`);
494
+ }
495
+ return result.output.requestId;
363
496
  }
364
497
  };
365
498
  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.64",
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": {