@vertesia/workflow 1.5.0-dev.20260717.131047Z → 1.5.0-dev.20260722.120446Z
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/lib/activities/generateDocumentProperties.d.ts +54 -13
- package/lib/activities/generateDocumentProperties.d.ts.map +1 -1
- package/lib/activities/generateDocumentProperties.js +109 -34
- package/lib/activities/generateDocumentProperties.js.map +1 -1
- package/lib/activities/generateOrAssignContentType.d.ts +6 -16
- package/lib/activities/generateOrAssignContentType.d.ts.map +1 -1
- package/lib/activities/generateOrAssignContentType.js +70 -19
- package/lib/activities/generateOrAssignContentType.js.map +1 -1
- package/lib/activities/media/exec.d.ts +60 -0
- package/lib/activities/media/exec.d.ts.map +1 -0
- package/lib/activities/media/exec.js +212 -0
- package/lib/activities/media/exec.js.map +1 -0
- package/lib/activities/media/prepareAudio.d.ts.map +1 -1
- package/lib/activities/media/prepareAudio.js +7 -6
- package/lib/activities/media/prepareAudio.js.map +1 -1
- package/lib/activities/media/prepareVideo.d.ts +5 -0
- package/lib/activities/media/prepareVideo.d.ts.map +1 -1
- package/lib/activities/media/prepareVideo.js +37 -13
- package/lib/activities/media/prepareVideo.js.map +1 -1
- package/lib/activities/media/probeMediaStreams.d.ts.map +1 -1
- package/lib/activities/media/probeMediaStreams.js +7 -6
- package/lib/activities/media/probeMediaStreams.js.map +1 -1
- package/lib/activities/renditions/generateVideoRendition.d.ts.map +1 -1
- package/lib/activities/renditions/generateVideoRendition.js +7 -5
- package/lib/activities/renditions/generateVideoRendition.js.map +1 -1
- package/lib/dsl/dsl-workflow.js +3 -0
- package/lib/dsl/dsl-workflow.js.map +1 -1
- package/lib/workflows-bundle.js +16277 -6846
- package/package.json +9 -9
- package/src/activities/generateDocumentProperties.test.ts +642 -0
- package/src/activities/generateDocumentProperties.ts +187 -43
- package/src/activities/generateOrAssignContentType.test.ts +196 -0
- package/src/activities/generateOrAssignContentType.ts +115 -30
- package/src/activities/media/exec.test.ts +194 -0
- package/src/activities/media/exec.ts +265 -0
- package/src/activities/media/prepareAudio.ts +12 -7
- package/src/activities/media/prepareVideo.test.ts +27 -0
- package/src/activities/media/prepareVideo.ts +54 -18
- package/src/activities/media/probeMediaStreams.test.ts +7 -8
- package/src/activities/media/probeMediaStreams.ts +11 -7
- package/src/activities/renditions/generateVideoRendition.ts +12 -6
- package/src/dsl/dsl-workflow.test.ts +4 -0
- package/src/dsl/dsl-workflow.ts +3 -0
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { ApplicationFailure, log } from '@temporalio/activity';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type ContentMetadata,
|
|
4
|
+
type ContentSource,
|
|
5
|
+
type DSLActivityExecutionPayload,
|
|
6
|
+
type DSLActivitySpec,
|
|
7
|
+
type JSONObject,
|
|
8
|
+
mergePreservingNonExtractable,
|
|
9
|
+
PDF_RENDITION_NAME,
|
|
10
|
+
type Rendition,
|
|
11
|
+
schemaForExtraction,
|
|
12
|
+
} from '@vertesia/common';
|
|
3
13
|
import { setupActivity } from '../dsl/setup/ActivityContext.js';
|
|
14
|
+
import { md5 } from '../utils/blobs.js';
|
|
4
15
|
import { type TruncateSpec, truncByMaxTokens } from '../utils/tokens.js';
|
|
5
16
|
import { executeInteractionFromActivity, type InteractionExecutionParams } from './executeInteraction.js';
|
|
6
17
|
|
|
7
18
|
const INT_EXTRACT_INFORMATION = 'sys:ExtractInformation';
|
|
8
19
|
|
|
20
|
+
export type GenerateDocumentPropertiesSource = 'auto' | 'text' | 'vision' | 'mixed';
|
|
21
|
+
|
|
9
22
|
interface RetryableError extends Error {
|
|
10
23
|
retryable?: boolean;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
interface GeneratedPropertiesSummary {
|
|
14
|
-
title?: unknown;
|
|
15
|
-
description?: unknown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
26
|
function toRetryableError(error: unknown): RetryableError {
|
|
19
27
|
if (error instanceof Error) {
|
|
20
28
|
return error as RetryableError;
|
|
@@ -31,57 +39,209 @@ export interface GenerateDocumentPropertiesParams extends InteractionExecutionPa
|
|
|
31
39
|
|
|
32
40
|
interactionName?: string;
|
|
33
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @deprecated Use source: 'mixed'. Accepted for backward compatibility with existing DSL callers.
|
|
44
|
+
*/
|
|
34
45
|
use_vision?: boolean;
|
|
46
|
+
|
|
47
|
+
source?: GenerateDocumentPropertiesSource;
|
|
48
|
+
|
|
49
|
+
instructions?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scoped page-image evidence prepared by intake (`prepareVisionEvidence` scratch refs).
|
|
53
|
+
* When the param is PRESENT (even as an empty array) and the source calls for visual
|
|
54
|
+
* evidence, these refs are the ONLY visual evidence used — never the whole-object
|
|
55
|
+
* `store:{id}` ref, which resolves to ALL pages near-full-res through the rendition
|
|
56
|
+
* generate-and-poll path. An empty array means intake prepared no usable evidence
|
|
57
|
+
* (budget-impossible or no renderable source): extraction then runs text-only or fails
|
|
58
|
+
* with no-source instead of shipping an over-budget payload.
|
|
59
|
+
* Absent = legacy behavior for existing DSL callers.
|
|
60
|
+
*/
|
|
61
|
+
evidence_images?: ContentSource[];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Opt-in freshness guard. When true, the activity computes a fingerprint of the extraction
|
|
65
|
+
* inputs (content etag, type + its object schema, source, instructions, interaction name)
|
|
66
|
+
* and returns `{ status: 'skipped' }` without executing the interaction when the fingerprint
|
|
67
|
+
* stored by a previous successful extraction matches and the object already has non-empty
|
|
68
|
+
* properties. Defaults to false so shared DSL callers keep the always-extract behavior.
|
|
69
|
+
* `payload.vars.forceGeneration` bypasses the skip.
|
|
70
|
+
*/
|
|
71
|
+
skip_if_fresh?: boolean;
|
|
35
72
|
}
|
|
36
73
|
export interface GenerateDocumentProperties extends DSLActivitySpec<GenerateDocumentPropertiesParams> {
|
|
37
74
|
name: 'generateDocumentProperties';
|
|
38
75
|
}
|
|
76
|
+
export type GenerateDocumentPropertiesResult =
|
|
77
|
+
| { status: 'completed' }
|
|
78
|
+
| { status: 'failed'; error: string }
|
|
79
|
+
| { document: string; status: 'skipped'; message: string };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stable fingerprint of the inputs that determine the extraction output. Key order is fixed by
|
|
83
|
+
* the object literal so the hash is deterministic.
|
|
84
|
+
*/
|
|
85
|
+
export function computeExtractionFingerprint(input: {
|
|
86
|
+
content_etag?: string;
|
|
87
|
+
type_id?: string;
|
|
88
|
+
source: GenerateDocumentPropertiesSource;
|
|
89
|
+
instructions?: string;
|
|
90
|
+
interactionName: string;
|
|
91
|
+
/** The type's object_schema: a schema edit under the same type id must invalidate the fingerprint. */
|
|
92
|
+
object_schema?: unknown;
|
|
93
|
+
/** Scoped vision evidence refs: a different page/profile selection must invalidate the fingerprint.
|
|
94
|
+
* Undefined (legacy callers) keeps previously stored hashes valid. */
|
|
95
|
+
evidence?: string[];
|
|
96
|
+
/** The object's text etag: text can change under the same content etag (re-render,
|
|
97
|
+
* re-conversion, manual edit) and text-consuming extractions must invalidate.
|
|
98
|
+
* Undefined (vision-only callers) keeps previously stored hashes valid. */
|
|
99
|
+
text_etag?: string | null;
|
|
100
|
+
}): string {
|
|
101
|
+
return md5(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
content_etag: input.content_etag,
|
|
104
|
+
type_id: input.type_id,
|
|
105
|
+
source: input.source,
|
|
106
|
+
instructions: input.instructions,
|
|
107
|
+
interactionName: input.interactionName,
|
|
108
|
+
object_schema: input.object_schema,
|
|
109
|
+
...(input.evidence ? { evidence: input.evidence } : {}),
|
|
110
|
+
...(input.text_etag !== undefined ? { text_etag: input.text_etag } : {}),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The fingerprint is persisted inside the generation run info of the last successful extraction
|
|
117
|
+
* (the server appends it to `metadata.generation_runs`). Other writers append runs without a
|
|
118
|
+
* fingerprint, so scan backwards for the most recent extraction entry.
|
|
119
|
+
*/
|
|
120
|
+
function getStoredExtractionFingerprint(metadata: ContentMetadata | undefined): string | undefined {
|
|
121
|
+
const runs = metadata?.generation_runs;
|
|
122
|
+
if (!Array.isArray(runs)) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
126
|
+
if (runs[i]?.extraction_fingerprint) {
|
|
127
|
+
return runs[i].extraction_fingerprint;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getPdfRenditionContent(doc: { metadata?: { renditions?: unknown } }): ContentSource | undefined {
|
|
134
|
+
const renditions = doc.metadata?.renditions;
|
|
135
|
+
if (!Array.isArray(renditions)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const pdfRendition = renditions.find(
|
|
139
|
+
(rendition): rendition is Rendition =>
|
|
140
|
+
typeof rendition === 'object' &&
|
|
141
|
+
rendition !== null &&
|
|
142
|
+
(rendition as { name?: unknown }).name === PDF_RENDITION_NAME &&
|
|
143
|
+
typeof (rendition as { content?: { source?: unknown } }).content?.source === 'string',
|
|
144
|
+
);
|
|
145
|
+
return pdfRendition?.content;
|
|
146
|
+
}
|
|
39
147
|
|
|
40
148
|
export async function generateDocumentProperties(
|
|
41
149
|
payload: DSLActivityExecutionPayload<GenerateDocumentPropertiesParams>,
|
|
42
|
-
) {
|
|
150
|
+
): Promise<GenerateDocumentPropertiesResult> {
|
|
43
151
|
const context = await setupActivity<GenerateDocumentPropertiesParams>(payload);
|
|
44
152
|
const { params, client, objectId } = context;
|
|
45
153
|
const interactionName = params.interactionName ?? INT_EXTRACT_INFORMATION;
|
|
46
154
|
|
|
47
155
|
const project = await context.fetchProject();
|
|
48
156
|
|
|
49
|
-
const doc = await client.objects.retrieve(objectId, '+text');
|
|
157
|
+
const doc = await client.objects.retrieve(objectId, '+text +metadata +content');
|
|
50
158
|
const type = doc.type ? await client.types.catalog.resolve(doc.type) : undefined;
|
|
51
159
|
|
|
52
|
-
if (!doc?.text && !params.use_vision && !doc?.content?.type?.startsWith('image/')) {
|
|
53
|
-
log.warn(`Object ${objectId} not found or text is empty`);
|
|
54
|
-
return { status: 'failed', error: 'no-text' };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
160
|
if (!type?.object_schema) {
|
|
58
161
|
log.info(`Object ${objectId} has no schema`);
|
|
59
162
|
return { document: objectId, status: 'skipped', message: 'no schema defined on type' };
|
|
60
163
|
}
|
|
61
164
|
|
|
62
|
-
const
|
|
165
|
+
const source = params.source ?? (params.use_vision ? 'mixed' : 'auto');
|
|
166
|
+
const evidenceProvided = Array.isArray(params.evidence_images);
|
|
167
|
+
const evidenceImages = params.evidence_images?.length ? params.evidence_images : undefined;
|
|
168
|
+
|
|
169
|
+
const extractionFingerprint = computeExtractionFingerprint({
|
|
170
|
+
content_etag: doc.content?.etag,
|
|
171
|
+
type_id: type.id,
|
|
172
|
+
source,
|
|
173
|
+
instructions: params.instructions,
|
|
174
|
+
interactionName,
|
|
175
|
+
object_schema: type.object_schema,
|
|
176
|
+
evidence: evidenceImages?.map((image) => image.source ?? image.name ?? ''),
|
|
177
|
+
// text may change under the same content etag; only vision-only extraction ignores it
|
|
178
|
+
text_etag: source === 'vision' ? undefined : (doc.text_etag ?? null),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (params.skip_if_fresh && !payload.vars?.forceGeneration) {
|
|
182
|
+
const storedFingerprint = getStoredExtractionFingerprint(doc.metadata);
|
|
183
|
+
const hasProperties = !!doc.properties && Object.keys(doc.properties).length > 0;
|
|
184
|
+
if (storedFingerprint === extractionFingerprint && hasProperties) {
|
|
185
|
+
log.info(`Skipping property extraction for ${objectId}: extraction inputs unchanged`, {
|
|
186
|
+
extractionFingerprint,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
document: objectId,
|
|
190
|
+
status: 'skipped',
|
|
191
|
+
message: 'properties are fresh (extraction fingerprint matches)',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const getImageRef = (): string | ContentSource | undefined => {
|
|
63
197
|
if (doc.content?.type?.startsWith('image/')) {
|
|
64
198
|
return `store:${doc.id}`;
|
|
65
199
|
}
|
|
66
200
|
|
|
67
|
-
if (
|
|
201
|
+
if (doc.content?.type?.startsWith('application/pdf')) {
|
|
68
202
|
return `store:${doc.id}`;
|
|
69
203
|
}
|
|
70
204
|
|
|
205
|
+
const pdfRendition = getPdfRenditionContent(doc);
|
|
206
|
+
if (pdfRendition?.type?.startsWith('application/pdf')) {
|
|
207
|
+
return pdfRendition;
|
|
208
|
+
}
|
|
209
|
+
|
|
71
210
|
log.info(`Object ${objectId} is not an image or pdf`);
|
|
72
211
|
return undefined;
|
|
73
212
|
};
|
|
74
213
|
|
|
75
|
-
const
|
|
214
|
+
const hasRealText = !!doc.text?.trim();
|
|
215
|
+
const content =
|
|
216
|
+
hasRealText && (source === 'auto' || source === 'text' || source === 'mixed')
|
|
217
|
+
? truncByMaxTokens(doc.text ?? '', params.truncate || 30000)
|
|
218
|
+
: undefined;
|
|
219
|
+
const needsVisualEvidence = source === 'vision' || source === 'mixed' || (source === 'auto' && !content);
|
|
220
|
+
// Scoped scratch page refs win over the whole-object store: ref (which resolves to ALL
|
|
221
|
+
// pages near-full-res through the rendition generate-and-poll path). A PRESENT param —
|
|
222
|
+
// even empty — disables the store-ref fallback entirely: intake decided what evidence
|
|
223
|
+
// (if any) may be sent.
|
|
224
|
+
const scopedImages = needsVisualEvidence ? evidenceImages : undefined;
|
|
225
|
+
const imageRef = needsVisualEvidence && !evidenceProvided ? getImageRef() : undefined;
|
|
226
|
+
|
|
227
|
+
if (!content && !imageRef && !scopedImages) {
|
|
228
|
+
log.warn(`Object ${objectId} has no text or supported vision source`);
|
|
229
|
+
return { status: 'failed', error: 'no-source' };
|
|
230
|
+
}
|
|
76
231
|
|
|
77
232
|
const promptData = {
|
|
78
233
|
content: content,
|
|
79
|
-
image:
|
|
234
|
+
image: imageRef,
|
|
235
|
+
images: scopedImages,
|
|
236
|
+
extraction_instructions: params.instructions,
|
|
80
237
|
human_context: project?.configuration?.human_context ?? undefined,
|
|
81
238
|
};
|
|
82
239
|
|
|
240
|
+
// Strip properties marked x-extract:false so constrained decoding cannot invent them.
|
|
241
|
+
const extractionSchema = schemaForExtraction(type.object_schema);
|
|
242
|
+
|
|
83
243
|
log.info(
|
|
84
|
-
`
|
|
244
|
+
`Extracting information from object ${objectId} with type ${type.name}`,
|
|
85
245
|
payload.debug_mode ? { params } : undefined,
|
|
86
246
|
);
|
|
87
247
|
|
|
@@ -93,7 +253,7 @@ export async function generateDocumentProperties(
|
|
|
93
253
|
{
|
|
94
254
|
...params,
|
|
95
255
|
include_previous_error: true,
|
|
96
|
-
result_schema:
|
|
256
|
+
result_schema: extractionSchema,
|
|
97
257
|
validate_result: type.strict_mode,
|
|
98
258
|
},
|
|
99
259
|
promptData,
|
|
@@ -125,38 +285,22 @@ export async function generateDocumentProperties(
|
|
|
125
285
|
throw error;
|
|
126
286
|
}
|
|
127
287
|
|
|
128
|
-
const getText = () => {
|
|
129
|
-
if (doc.text) {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
let text = '';
|
|
133
|
-
const jsonResult = infoRes.result.object<GeneratedPropertiesSummary>();
|
|
134
|
-
if (typeof jsonResult.title === 'string') {
|
|
135
|
-
text += `${jsonResult.title}\n`;
|
|
136
|
-
}
|
|
137
|
-
if (typeof jsonResult.description === 'string') {
|
|
138
|
-
text += jsonResult.description;
|
|
139
|
-
}
|
|
140
|
-
if (text) {
|
|
141
|
-
return text;
|
|
142
|
-
} else {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
288
|
log.info(`Extracted information from object ${objectId} with type ${type.name}`, { runId: infoRes.id });
|
|
289
|
+
const extracted = infoRes.result.object<JSONObject>() ?? {};
|
|
290
|
+
const existing =
|
|
291
|
+
doc.properties && typeof doc.properties === 'object' && !Array.isArray(doc.properties)
|
|
292
|
+
? (doc.properties as Record<string, unknown>)
|
|
293
|
+
: {};
|
|
294
|
+
const properties = mergePreservingNonExtractable(existing, extracted, type.object_schema) as JSONObject;
|
|
148
295
|
await client.objects.update(
|
|
149
296
|
doc.id,
|
|
150
297
|
{
|
|
151
|
-
properties
|
|
152
|
-
...infoRes.result.object<JSONObject>(),
|
|
153
|
-
...(doc.text_etag !== undefined ? { etag: doc.text_etag } : {}),
|
|
154
|
-
},
|
|
155
|
-
text: getText(),
|
|
298
|
+
properties,
|
|
156
299
|
generation_run_info: {
|
|
157
300
|
id: infoRes.id,
|
|
158
301
|
date: new Date().toISOString(),
|
|
159
302
|
model: infoRes.modelId ?? '',
|
|
303
|
+
extraction_fingerprint: extractionFingerprint,
|
|
160
304
|
},
|
|
161
305
|
},
|
|
162
306
|
{ suppressWorkflows: true },
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { MockActivityEnvironment } from '@temporalio/testing';
|
|
2
|
+
import type { VertesiaClient } from '@vertesia/client';
|
|
3
|
+
import {
|
|
4
|
+
ContentEventName,
|
|
5
|
+
type ContentObjectTypeItem,
|
|
6
|
+
type DSLActivityExecutionPayload,
|
|
7
|
+
PDF_RENDITION_NAME,
|
|
8
|
+
type Rendition,
|
|
9
|
+
} from '@vertesia/common';
|
|
10
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import type { ActivityContext } from '../dsl/setup/ActivityContext.js';
|
|
12
|
+
import { executeInteractionFromActivity } from './executeInteraction.js';
|
|
13
|
+
import {
|
|
14
|
+
type GenerateOrAssignContentTypeParams,
|
|
15
|
+
type GenerateOrAssignContentTypeResult,
|
|
16
|
+
generateOrAssignContentType,
|
|
17
|
+
} from './generateOrAssignContentType.js';
|
|
18
|
+
|
|
19
|
+
vi.mock('../dsl/setup/ActivityContext.js', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import('../dsl/setup/ActivityContext.js')>();
|
|
21
|
+
return { ...actual, setupActivity: vi.fn() };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('./executeInteraction.js', async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal<typeof import('./executeInteraction.js')>();
|
|
26
|
+
return { ...actual, executeInteractionFromActivity: vi.fn() };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let testEnv: MockActivityEnvironment;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
testEnv = new MockActivityEnvironment();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function payload(
|
|
40
|
+
params: GenerateOrAssignContentTypeParams,
|
|
41
|
+
): DSLActivityExecutionPayload<GenerateOrAssignContentTypeParams> {
|
|
42
|
+
return {
|
|
43
|
+
account_id: 'test-account',
|
|
44
|
+
activity: { name: 'generateOrAssignContentType', params: {} },
|
|
45
|
+
auth_token: 'mock-token',
|
|
46
|
+
config: { studio_url: 'http://mock-studio', store_url: 'http://mock-store' },
|
|
47
|
+
event: ContentEventName.create,
|
|
48
|
+
input: { inputType: 'objectIds', objectIds: ['object-1'] },
|
|
49
|
+
objectIds: ['object-1'],
|
|
50
|
+
params,
|
|
51
|
+
project_id: 'test-project',
|
|
52
|
+
vars: {},
|
|
53
|
+
workflow_name: 'test-workflow',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function typeItem(name: string, status?: ContentObjectTypeItem['status']): ContentObjectTypeItem {
|
|
58
|
+
return {
|
|
59
|
+
id: `type-${name}`,
|
|
60
|
+
name,
|
|
61
|
+
status,
|
|
62
|
+
} as ContentObjectTypeItem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mockSelectionResult(documentType: string) {
|
|
66
|
+
vi.mocked(executeInteractionFromActivity).mockResolvedValue({
|
|
67
|
+
id: 'run-1',
|
|
68
|
+
modelId: 'model-1',
|
|
69
|
+
result: {
|
|
70
|
+
object: vi.fn(() => ({ document_type: documentType })),
|
|
71
|
+
},
|
|
72
|
+
} as unknown as Awaited<ReturnType<typeof executeInteractionFromActivity>>);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function mockSetup(
|
|
76
|
+
types: ContentObjectTypeItem[],
|
|
77
|
+
options: {
|
|
78
|
+
contentType?: string;
|
|
79
|
+
renditions?: Rendition[];
|
|
80
|
+
text?: string;
|
|
81
|
+
} = {},
|
|
82
|
+
) {
|
|
83
|
+
const { setupActivity } = await import('../dsl/setup/ActivityContext.js');
|
|
84
|
+
const update = vi.fn(() => Promise.resolve({}));
|
|
85
|
+
const retrieveObject = vi.fn(() =>
|
|
86
|
+
Promise.resolve({
|
|
87
|
+
id: 'object-1',
|
|
88
|
+
content: { type: options.contentType ?? 'text/plain' },
|
|
89
|
+
metadata: { renditions: options.renditions },
|
|
90
|
+
text: options.text ?? 'Document text',
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
const listTypes = vi.fn(() => Promise.resolve(types));
|
|
94
|
+
|
|
95
|
+
vi.mocked(setupActivity).mockImplementation((activityPayload) =>
|
|
96
|
+
Promise.resolve({
|
|
97
|
+
client: {
|
|
98
|
+
objects: {
|
|
99
|
+
retrieve: retrieveObject,
|
|
100
|
+
update,
|
|
101
|
+
},
|
|
102
|
+
types: {
|
|
103
|
+
catalog: {
|
|
104
|
+
list: listTypes,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
} as unknown as VertesiaClient,
|
|
108
|
+
fetchProject: vi.fn(() => Promise.resolve({ configuration: {} })),
|
|
109
|
+
objectId: 'object-1',
|
|
110
|
+
params: activityPayload.params,
|
|
111
|
+
} as unknown as ActivityContext<GenerateOrAssignContentTypeParams>),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return { listTypes, update };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('generateOrAssignContentType', () => {
|
|
118
|
+
it('excludes draft types from selection and does not assign them if returned by the model', async () => {
|
|
119
|
+
const { update } = await mockSetup([
|
|
120
|
+
typeItem('Invoice', 'active'),
|
|
121
|
+
typeItem('DraftInvoice', 'draft'),
|
|
122
|
+
typeItem('DocumentPart', 'active'),
|
|
123
|
+
typeItem('GenericDocument', 'active'),
|
|
124
|
+
typeItem('Rendition', 'active'),
|
|
125
|
+
]);
|
|
126
|
+
mockSelectionResult('DraftInvoice');
|
|
127
|
+
|
|
128
|
+
const result: GenerateOrAssignContentTypeResult = await testEnv.run(
|
|
129
|
+
generateOrAssignContentType,
|
|
130
|
+
payload({ allowNewContentTypes: false }),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
134
|
+
expect.anything(),
|
|
135
|
+
'sys:SelectDocumentType',
|
|
136
|
+
expect.objectContaining({ allowNewContentTypes: false }),
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
existing_types: [
|
|
139
|
+
expect.objectContaining({ name: 'Invoice' }),
|
|
140
|
+
expect.objectContaining({ name: 'GenericDocument' }),
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
expect(update).toHaveBeenCalledWith('object-1', { type: 'sys:GenericDocument' }, { suppressWorkflows: true });
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
id: 'sys:GenericDocument',
|
|
147
|
+
isNew: false,
|
|
148
|
+
name: 'GenericDocument',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('uses a PDF rendition as visual evidence for PowerPoint type selection', async () => {
|
|
153
|
+
const pdfRendition = {
|
|
154
|
+
name: PDF_RENDITION_NAME,
|
|
155
|
+
content: {
|
|
156
|
+
name: 'slides.pdf',
|
|
157
|
+
source: 'renditions/source-etag/slides.pdf',
|
|
158
|
+
type: 'application/pdf',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const { update } = await mockSetup(
|
|
162
|
+
[typeItem('MarketPresentation', 'active'), typeItem('GenericDocument', 'active')],
|
|
163
|
+
{
|
|
164
|
+
contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
165
|
+
renditions: [pdfRendition],
|
|
166
|
+
text: '',
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
mockSelectionResult('MarketPresentation');
|
|
170
|
+
|
|
171
|
+
const result: GenerateOrAssignContentTypeResult = await testEnv.run(
|
|
172
|
+
generateOrAssignContentType,
|
|
173
|
+
payload({ allowNewContentTypes: false }),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
177
|
+
expect.anything(),
|
|
178
|
+
'sys:SelectDocumentType',
|
|
179
|
+
expect.objectContaining({ allowNewContentTypes: false }),
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
content: undefined,
|
|
182
|
+
image: pdfRendition.content,
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
expect(update).toHaveBeenCalledWith(
|
|
186
|
+
'object-1',
|
|
187
|
+
{ type: 'type-MarketPresentation' },
|
|
188
|
+
{ suppressWorkflows: true },
|
|
189
|
+
);
|
|
190
|
+
expect(result).toEqual({
|
|
191
|
+
id: 'type-MarketPresentation',
|
|
192
|
+
isNew: false,
|
|
193
|
+
name: 'MarketPresentation',
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|