@vertesia/workflow 1.4.0 → 1.5.0-dev.20260713.113008Z
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/detectDocumentLanguage.d.ts +28 -0
- package/lib/activities/detectDocumentLanguage.d.ts.map +1 -0
- package/lib/activities/detectDocumentLanguage.js +58 -0
- package/lib/activities/detectDocumentLanguage.js.map +1 -0
- package/lib/activities/extractDocumentText.js +12 -1
- package/lib/activities/extractDocumentText.js.map +1 -1
- package/lib/activities/generateEmbeddings.d.ts +4 -0
- package/lib/activities/generateEmbeddings.d.ts.map +1 -1
- package/lib/activities/generateEmbeddings.js +43 -39
- package/lib/activities/generateEmbeddings.js.map +1 -1
- package/lib/activities/generateOrAssignContentType.d.ts +10 -0
- package/lib/activities/generateOrAssignContentType.d.ts.map +1 -1
- package/lib/activities/generateOrAssignContentType.js +40 -3
- package/lib/activities/generateOrAssignContentType.js.map +1 -1
- package/lib/activities/handleError.d.ts.map +1 -1
- package/lib/activities/handleError.js +1 -0
- package/lib/activities/handleError.js.map +1 -1
- package/lib/activities/index-dsl.d.ts +1 -0
- package/lib/activities/index-dsl.d.ts.map +1 -1
- package/lib/activities/index-dsl.js +1 -0
- package/lib/activities/index-dsl.js.map +1 -1
- package/lib/activities/media/prepareAudio.d.ts.map +1 -1
- package/lib/activities/media/prepareAudio.js +4 -1
- package/lib/activities/media/prepareAudio.js.map +1 -1
- package/lib/activities/media/prepareVideo.d.ts.map +1 -1
- package/lib/activities/media/prepareVideo.js +4 -1
- package/lib/activities/media/prepareVideo.js.map +1 -1
- package/lib/activities/notifyWebhook.d.ts +2 -0
- package/lib/activities/notifyWebhook.d.ts.map +1 -1
- package/lib/activities/notifyWebhook.js +82 -12
- package/lib/activities/notifyWebhook.js.map +1 -1
- package/lib/bulk-import.d.ts +1 -0
- package/lib/bulk-import.d.ts.map +1 -1
- package/lib/conversion/TextractProcessor.js +4 -4
- package/lib/conversion/TextractProcessor.js.map +1 -1
- package/lib/conversion/image.d.ts.map +1 -1
- package/lib/conversion/image.js +2 -0
- package/lib/conversion/image.js.map +1 -1
- package/lib/dsl/setup/fetch/DataProvider.js +3 -3
- package/lib/dsl/setup/fetch/DataProvider.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/system/notifyWebhookWorkflow.d.ts +10 -3
- package/lib/system/notifyWebhookWorkflow.d.ts.map +1 -1
- package/lib/system/notifyWebhookWorkflow.js +19 -1
- package/lib/system/notifyWebhookWorkflow.js.map +1 -1
- package/lib/system/recalculateEmbeddingsWorkflow.d.ts +4 -0
- package/lib/system/recalculateEmbeddingsWorkflow.d.ts.map +1 -1
- package/lib/workflows-bundle.js +1925 -935
- package/package.json +17 -17
- package/src/activities/detectDocumentLanguage.ts +92 -0
- package/src/activities/extractDocumentText.ts +13 -1
- package/src/activities/generateEmbeddings.ts +44 -42
- package/src/activities/generateOrAssignContentType.ts +56 -3
- package/src/activities/handleError.ts +1 -0
- package/src/activities/index-dsl.ts +1 -0
- package/src/activities/media/prepareAudio.ts +4 -1
- package/src/activities/media/prepareVideo.ts +4 -1
- package/src/activities/media/probeMediaStreams.test.ts +1 -1
- package/src/activities/media/saveGladiaTranscription.test.ts +2 -1
- package/src/activities/media/transcribeMediaWithGladia.test.ts +1 -1
- package/src/activities/notifyWebhook.test.ts +119 -0
- package/src/activities/notifyWebhook.ts +86 -12
- package/src/bulk-import.ts +1 -0
- package/src/conversion/TextractProcessor.ts +4 -4
- package/src/conversion/image.test.ts +24 -0
- package/src/conversion/image.ts +3 -0
- package/src/dsl/setup/fetch/DataProvider.ts +3 -3
- package/src/index.ts +1 -0
- package/src/system/notifyWebhookWorkflow.ts +31 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-dev.20260713.113008Z",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vertesia workflow DSL",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
},
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@smithy/types": "^4.
|
|
18
|
-
"@temporalio/proto": "^1.
|
|
19
|
-
"@temporalio/testing": "^1.
|
|
20
|
-
"@temporalio/worker": "^1.
|
|
17
|
+
"@smithy/types": "^4.15.0",
|
|
18
|
+
"@temporalio/proto": "^1.19.0",
|
|
19
|
+
"@temporalio/testing": "^1.19.0",
|
|
20
|
+
"@temporalio/worker": "^1.19.0",
|
|
21
21
|
"@types/jsonwebtoken": "^9.0.10",
|
|
22
22
|
"@types/node": "^24.13.2",
|
|
23
23
|
"@types/papaparse": "^5.5.2",
|
|
@@ -27,29 +27,29 @@
|
|
|
27
27
|
"@vertesia/tsconfig": "0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@aws-sdk/client-s3": "^3.
|
|
31
|
-
"@aws-sdk/client-textract": "^3.
|
|
32
|
-
"@aws-sdk/credential-providers": "^3.
|
|
33
|
-
"@temporalio/activity": "^1.
|
|
34
|
-
"@temporalio/workflow": "^1.
|
|
30
|
+
"@aws-sdk/client-s3": "^3.1078.0",
|
|
31
|
+
"@aws-sdk/client-textract": "^3.1078.0",
|
|
32
|
+
"@aws-sdk/credential-providers": "^3.1078.0",
|
|
33
|
+
"@temporalio/activity": "^1.19.0",
|
|
34
|
+
"@temporalio/workflow": "^1.19.0",
|
|
35
35
|
"fast-deep-equal": "^3.1.3",
|
|
36
36
|
"jsonwebtoken": "^9.0.3",
|
|
37
37
|
"mime": "^4.1.0",
|
|
38
38
|
"ms": "3.0.0-canary.202508261828",
|
|
39
39
|
"node-web-stream-adapters": "^0.2.1",
|
|
40
40
|
"p-limit": "^7.3.0",
|
|
41
|
-
"papaparse": "^5.5.
|
|
41
|
+
"papaparse": "^5.5.4",
|
|
42
42
|
"seedrandom": "^3.0.5",
|
|
43
|
-
"sharp": "^0.
|
|
43
|
+
"sharp": "^0.35.3",
|
|
44
44
|
"tiktoken": "^1.0.22",
|
|
45
45
|
"tmp": "^0.2.7",
|
|
46
46
|
"tmp-promise": "^3.0.3",
|
|
47
|
-
"undici": "^7.
|
|
47
|
+
"undici": "^7.28.0",
|
|
48
48
|
"yaml": "^2.9.0",
|
|
49
|
-
"@llumiverse/common": "1.
|
|
50
|
-
"@vertesia/api-fetch-client": "1.
|
|
51
|
-
"@vertesia/client": "1.
|
|
52
|
-
"@vertesia/common": "1.
|
|
49
|
+
"@llumiverse/common": "1.5.0-dev",
|
|
50
|
+
"@vertesia/api-fetch-client": "1.5.0-dev.20260713.113008Z",
|
|
51
|
+
"@vertesia/client": "1.5.0-dev.20260713.113008Z",
|
|
52
|
+
"@vertesia/common": "1.5.0-dev.20260713.113008Z"
|
|
53
53
|
},
|
|
54
54
|
"exports": {
|
|
55
55
|
".": {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { log } from '@temporalio/activity';
|
|
2
|
+
import type { DSLActivityExecutionPayload, DSLActivitySpec } from '@vertesia/common';
|
|
3
|
+
import { setupActivity } from '../dsl/setup/ActivityContext.js';
|
|
4
|
+
import { executeInteractionFromActivity, type InteractionExecutionParams } from './executeInteraction.js';
|
|
5
|
+
|
|
6
|
+
const INT_DETECT_LANGUAGE = 'sys:DetectLanguage';
|
|
7
|
+
|
|
8
|
+
// A short head of the text is enough to identify the language; keep the call tiny.
|
|
9
|
+
const DEFAULT_MAX_CHARS = 500;
|
|
10
|
+
|
|
11
|
+
const LANGUAGE_RESULT_SCHEMA: InteractionExecutionParams['result_schema'] = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
languages: { type: 'array', items: { type: 'string' } },
|
|
15
|
+
},
|
|
16
|
+
required: ['languages'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface DetectDocumentLanguageParams extends InteractionExecutionParams {
|
|
20
|
+
interactionName?: string;
|
|
21
|
+
/** Number of leading characters of the document text to inspect (default 500). */
|
|
22
|
+
maxChars?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DetectDocumentLanguage extends DSLActivitySpec<DetectDocumentLanguageParams> {
|
|
26
|
+
name: 'detectDocumentLanguage';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DetectLanguageResult {
|
|
30
|
+
languages?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect the document language(s) from its first pages and store them in metadata.languages.
|
|
35
|
+
*
|
|
36
|
+
* A small, focused pass: it runs the sys:DetectLanguage interaction on a short text excerpt rather
|
|
37
|
+
* than folding language into full property extraction (more reliable, and cheap). No-op when the
|
|
38
|
+
* document has no text yet (e.g. media handled by the transcription path, which sets languages
|
|
39
|
+
* itself).
|
|
40
|
+
*/
|
|
41
|
+
export async function detectDocumentLanguage(payload: DSLActivityExecutionPayload<DetectDocumentLanguageParams>) {
|
|
42
|
+
const context = await setupActivity<DetectDocumentLanguageParams>(payload);
|
|
43
|
+
const { params, client, objectId } = context;
|
|
44
|
+
const interactionName = params.interactionName ?? INT_DETECT_LANGUAGE;
|
|
45
|
+
|
|
46
|
+
const doc = await client.objects.retrieve(objectId, '+text');
|
|
47
|
+
if (!doc?.text) {
|
|
48
|
+
log.info(`detectDocumentLanguage: object ${objectId} has no text, skipping`);
|
|
49
|
+
return { status: 'skipped', message: 'no-text' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const content = doc.text.slice(0, params.maxChars ?? DEFAULT_MAX_CHARS);
|
|
53
|
+
|
|
54
|
+
let res: Awaited<ReturnType<typeof executeInteractionFromActivity>>;
|
|
55
|
+
try {
|
|
56
|
+
res = await executeInteractionFromActivity(
|
|
57
|
+
client,
|
|
58
|
+
interactionName,
|
|
59
|
+
{ ...params, result_schema: LANGUAGE_RESULT_SCHEMA },
|
|
60
|
+
{ content },
|
|
61
|
+
payload.debug_mode ?? false,
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Language detection is best-effort — never fail the whole intake over it.
|
|
65
|
+
log.warn(`detectDocumentLanguage: detection failed for ${objectId}, skipping`, { error: String(error) });
|
|
66
|
+
return { status: 'skipped', message: 'detection-failed' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = res.result.object<DetectLanguageResult>();
|
|
70
|
+
const languages = Array.isArray(result.languages)
|
|
71
|
+
? Array.from(
|
|
72
|
+
new Set(
|
|
73
|
+
result.languages
|
|
74
|
+
.filter((lang): lang is string => typeof lang === 'string')
|
|
75
|
+
.map((lang) => lang.trim().toLowerCase())
|
|
76
|
+
.filter(Boolean),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
if (languages.length === 0) {
|
|
82
|
+
log.warn(`detectDocumentLanguage: no language detected for ${objectId}`, { runId: res.id });
|
|
83
|
+
return { status: 'completed', languages: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// metadata-only update (not source/properties), so it won't re-trigger intake; suppress
|
|
87
|
+
// workflows anyway for consistency with the other intake writes.
|
|
88
|
+
await client.objects.update(objectId, { metadata: { ...doc.metadata, languages } }, { suppressWorkflows: true });
|
|
89
|
+
|
|
90
|
+
log.info(`detectDocumentLanguage: set languages for ${objectId}`, { languages, runId: res.id });
|
|
91
|
+
return { status: 'completed', languages };
|
|
92
|
+
}
|
|
@@ -235,13 +235,25 @@ function createResponse(
|
|
|
235
235
|
return {
|
|
236
236
|
status,
|
|
237
237
|
message,
|
|
238
|
-
tokens: doc
|
|
238
|
+
tokens: getCompleteTokens(doc),
|
|
239
239
|
len: text.length,
|
|
240
240
|
objectId: doc.id,
|
|
241
241
|
hasText: !!text,
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function getCompleteTokens(doc: ContentObject): TextExtractionResult['tokens'] {
|
|
246
|
+
const tokens = doc.tokens;
|
|
247
|
+
if (tokens?.count === undefined || tokens.encoding === undefined || tokens.etag === undefined) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
count: tokens.count,
|
|
252
|
+
encoding: tokens.encoding,
|
|
253
|
+
etag: tokens.etag,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
245
257
|
function sniffIfText(buf: Buffer) {
|
|
246
258
|
// If file is too large, don't even try
|
|
247
259
|
if (buf.length > 500 * 1024) {
|
|
@@ -48,21 +48,33 @@ export interface GenerateEmbeddings extends DSLActivitySpec<GenerateEmbeddingsPa
|
|
|
48
48
|
name: 'generateEmbeddings';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* True when an error indicates the embedding environment is missing/unavailable (e.g. the
|
|
53
|
+
* configured environment id no longer exists). Treated as "not configured" so intake skips
|
|
54
|
+
* embeddings gracefully instead of failing.
|
|
55
|
+
*/
|
|
56
|
+
function isEnvironmentNotFound(error: unknown): boolean {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return /environment not found/i.test(message);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
export async function generateEmbeddings(payload: DSLActivityExecutionPayload<GenerateEmbeddingsParams>) {
|
|
52
62
|
const { params, client, objectId, fetchProject } = await setupActivity<GenerateEmbeddingsParams>(payload);
|
|
53
63
|
const { force, type } = params;
|
|
54
64
|
|
|
65
|
+
const skipped = (message: string) => {
|
|
66
|
+
log.info(`Skipping ${type} embeddings for ${objectId}: ${message}`);
|
|
67
|
+
return { id: objectId, status: 'skipped' as const, message };
|
|
68
|
+
};
|
|
69
|
+
|
|
55
70
|
const projectData = await fetchProject();
|
|
56
71
|
const config = projectData?.configuration.embeddings[type];
|
|
57
72
|
if (!projectData) {
|
|
58
73
|
throw new DocumentNotFoundError('Project not found', [payload.project_id]);
|
|
59
74
|
}
|
|
75
|
+
// Embeddings not configured for this type -> skip rather than fail intake.
|
|
60
76
|
if (!config) {
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!projectData) {
|
|
65
|
-
throw new DocumentNotFoundError('Project not found', [payload.project_id]);
|
|
77
|
+
return skipped(`embeddings not configured for type ${type}`);
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
if (!projectData?.configuration.embeddings[type]?.enabled) {
|
|
@@ -82,10 +94,9 @@ export async function generateEmbeddings(payload: DSLActivityExecutionPayload<Ge
|
|
|
82
94
|
config,
|
|
83
95
|
});
|
|
84
96
|
|
|
97
|
+
// No embedding environment configured for this type -> skip rather than fail intake.
|
|
85
98
|
if (!config.environment) {
|
|
86
|
-
|
|
87
|
-
'No environment found in project configuration. Set environment in project configuration to generate embeddings.',
|
|
88
|
-
);
|
|
99
|
+
return skipped(`no embedding environment configured for type ${type}`);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
let document: Awaited<ReturnType<typeof client.objects.retrieve>>;
|
|
@@ -111,40 +122,31 @@ export async function generateEmbeddings(payload: DSLActivityExecutionPayload<Ge
|
|
|
111
122
|
| Awaited<ReturnType<typeof generateImageEmbeddings>>
|
|
112
123
|
| { id: string; status: string; message: string };
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
client,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
force,
|
|
140
|
-
});
|
|
141
|
-
break;
|
|
142
|
-
default:
|
|
143
|
-
res = {
|
|
144
|
-
id: objectId,
|
|
145
|
-
status: 'failed',
|
|
146
|
-
message: `unsupported embedding type: ${type}`,
|
|
147
|
-
};
|
|
125
|
+
try {
|
|
126
|
+
switch (type) {
|
|
127
|
+
case SupportedEmbeddingTypes.text:
|
|
128
|
+
res = await generateTextEmbeddings({ client, config, document, type, force });
|
|
129
|
+
break;
|
|
130
|
+
case SupportedEmbeddingTypes.properties:
|
|
131
|
+
res = await generateTextEmbeddings({ client, config, document, type, force });
|
|
132
|
+
break;
|
|
133
|
+
case SupportedEmbeddingTypes.image:
|
|
134
|
+
res = await generateImageEmbeddings({ client, config, document, type, force });
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
res = {
|
|
138
|
+
id: objectId,
|
|
139
|
+
status: 'failed',
|
|
140
|
+
message: `unsupported embedding type: ${type}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Configured environment is missing/unavailable -> skip rather than fail the whole intake
|
|
145
|
+
// (and avoid the long LLM-activity retry storm a throw would trigger).
|
|
146
|
+
if (isEnvironmentNotFound(error)) {
|
|
147
|
+
return skipped(`embedding environment unavailable for type ${type}`);
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
return res;
|
|
@@ -13,6 +13,12 @@ import { executeInteractionFromActivity, type InteractionExecutionParams } from
|
|
|
13
13
|
const INT_SELECT_DOCUMENT_TYPE = 'sys:SelectDocumentType';
|
|
14
14
|
const INT_GENERATE_METADATA_MODEL = 'sys:GenerateMetadataModel';
|
|
15
15
|
|
|
16
|
+
// Always-present system fallback type (registered in zeno's SystemContentTypeRegistry). When type
|
|
17
|
+
// selection finds no existing match and new-type generation is disabled, documents are assigned
|
|
18
|
+
// this generic type so they still get a minimal property set + a processing hint for later typing.
|
|
19
|
+
const GENERIC_DOCUMENT_TYPE_ID = 'sys:GenericDocument';
|
|
20
|
+
const GENERIC_DOCUMENT_TYPE_NAME = 'GenericDocument';
|
|
21
|
+
|
|
16
22
|
interface RetryableError extends Error {
|
|
17
23
|
retryable?: boolean;
|
|
18
24
|
}
|
|
@@ -51,6 +57,18 @@ export interface GenerateOrAssignContentTypeParams extends InteractionExecutionP
|
|
|
51
57
|
selectDocumentType?: string;
|
|
52
58
|
generateMetadataModel?: string;
|
|
53
59
|
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether the activity may create a brand-new content type when no existing type matches.
|
|
63
|
+
* Defaults to true for backward compatibility.
|
|
64
|
+
*/
|
|
65
|
+
allowNewContentTypes?: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Type id to assign when no existing type matches and generation is disabled. Resolved in this
|
|
69
|
+
* project; falls back to sys:GenericDocument if unset or unresolvable.
|
|
70
|
+
*/
|
|
71
|
+
fallbackTypeId?: string;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
export interface GenerateOrAssignContentType extends DSLActivitySpec<GenerateOrAssignContentTypeParams> {
|
|
@@ -167,9 +185,16 @@ export async function generateOrAssignContentType(
|
|
|
167
185
|
selectedType = types.find((t) => t.name === jsonResult.document_type);
|
|
168
186
|
|
|
169
187
|
if (!selectedType) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
if (params.allowNewContentTypes === false) {
|
|
189
|
+
// Type generation is disabled (handled separately, e.g. via the Studio Assistant), so
|
|
190
|
+
// fall back to the project's default type (or sys:GenericDocument) rather than leaving
|
|
191
|
+
// the document untyped.
|
|
192
|
+
selectedType = await resolveFallbackType(context, params.fallbackTypeId, jsonResult.document_type);
|
|
193
|
+
} else {
|
|
194
|
+
log.warn('Document type not identified: starting type generation');
|
|
195
|
+
const newType = await generateNewType(context, existing_types, content, fileRef);
|
|
196
|
+
selectedType = { id: newType.id, name: newType.name };
|
|
197
|
+
}
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
if (!selectedType) {
|
|
@@ -189,6 +214,34 @@ export async function generateOrAssignContentType(
|
|
|
189
214
|
};
|
|
190
215
|
}
|
|
191
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the fallback type to assign when selection finds no match: the project's configured
|
|
219
|
+
* default content type if set and resolvable, otherwise the platform sys:GenericDocument.
|
|
220
|
+
*/
|
|
221
|
+
async function resolveFallbackType(
|
|
222
|
+
context: ActivityContext<GenerateOrAssignContentTypeParams>,
|
|
223
|
+
fallbackTypeId: string | undefined,
|
|
224
|
+
selectedDocumentType: unknown,
|
|
225
|
+
): Promise<{ id: string; name: string }> {
|
|
226
|
+
if (fallbackTypeId && fallbackTypeId !== GENERIC_DOCUMENT_TYPE_ID) {
|
|
227
|
+
try {
|
|
228
|
+
const resolved = await context.client.types.catalog.resolve(fallbackTypeId);
|
|
229
|
+
log.info('Document type not identified; assigning project default content type', {
|
|
230
|
+
fallbackTypeId,
|
|
231
|
+
selectedDocumentType,
|
|
232
|
+
});
|
|
233
|
+
return { id: resolved.id ?? fallbackTypeId, name: resolved.name ?? fallbackTypeId };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
log.warn('Configured default content type not resolvable; using GenericDocument', {
|
|
236
|
+
fallbackTypeId,
|
|
237
|
+
error: error instanceof Error ? error.message : String(error),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
log.info('Document type not identified; assigning GenericDocument fallback', { selectedDocumentType });
|
|
242
|
+
return { id: GENERIC_DOCUMENT_TYPE_ID, name: GENERIC_DOCUMENT_TYPE_NAME };
|
|
243
|
+
}
|
|
244
|
+
|
|
192
245
|
async function generateNewType(
|
|
193
246
|
context: ActivityContext<GenerateOrAssignContentTypeParams>,
|
|
194
247
|
existing_types: ContentObjectTypeItem[],
|
|
@@ -10,6 +10,7 @@ export async function handleDslError(payload: DSLActivityExecutionPayload<Handle
|
|
|
10
10
|
const { client, params, objectId } = await setupActivity<HandleDslErrorParams>(payload);
|
|
11
11
|
|
|
12
12
|
const isIntake = [
|
|
13
|
+
'StandardIntake',
|
|
13
14
|
'StandardDocumentIntake',
|
|
14
15
|
'StandardImageIntake',
|
|
15
16
|
'StandardMediaContainerIntake',
|
|
@@ -5,6 +5,7 @@ export { createOrUpdateDocumentFromInteractionRun } from './advanced/createOrUpd
|
|
|
5
5
|
export { updateDocumentFromInteractionRun } from './advanced/updateDocumentFromInteractionRun.js';
|
|
6
6
|
export { chunkDocument } from './chunkDocument.js';
|
|
7
7
|
export { createPdfDocumentFromSource } from './createDocumentFromOther.js';
|
|
8
|
+
export { detectDocumentLanguage } from './detectDocumentLanguage.js';
|
|
8
9
|
export { executeInteraction } from './executeInteraction.js';
|
|
9
10
|
export { executeRemoteActivity } from './executeRemoteActivity.js';
|
|
10
11
|
export { extractDocumentText } from './extractDocumentText.js';
|
|
@@ -258,11 +258,14 @@ export async function prepareAudio(
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
// Step 4: Update content object with metadata and renditions
|
|
261
|
+
const generationRuns = Array.isArray(inputObject.metadata?.generation_runs)
|
|
262
|
+
? inputObject.metadata.generation_runs
|
|
263
|
+
: [];
|
|
261
264
|
const audioMetadata: AudioMetadata = {
|
|
262
265
|
type: ContentNature.Audio,
|
|
263
266
|
duration: metadata.duration,
|
|
264
267
|
renditions,
|
|
265
|
-
generation_runs:
|
|
268
|
+
generation_runs: generationRuns,
|
|
266
269
|
};
|
|
267
270
|
|
|
268
271
|
await client.objects.update(objectId, {
|
|
@@ -534,6 +534,9 @@ export async function prepareVideo(
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
// Step 7: Update content object with metadata and renditions
|
|
537
|
+
const generationRuns = Array.isArray(inputObject.metadata?.generation_runs)
|
|
538
|
+
? inputObject.metadata.generation_runs
|
|
539
|
+
: [];
|
|
537
540
|
const videoMetadata: VideoMetadata = {
|
|
538
541
|
type: ContentNature.Video,
|
|
539
542
|
duration: metadata.duration,
|
|
@@ -543,7 +546,7 @@ export async function prepareVideo(
|
|
|
543
546
|
},
|
|
544
547
|
renditions,
|
|
545
548
|
hasAudio: metadata.hasAudio,
|
|
546
|
-
generation_runs:
|
|
549
|
+
generation_runs: generationRuns,
|
|
547
550
|
};
|
|
548
551
|
|
|
549
552
|
await client.objects.update(objectId, {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { MockActivityEnvironment } from '@temporalio/testing';
|
|
2
2
|
import type { VertesiaClient } from '@vertesia/client';
|
|
3
3
|
import { ContentEventName, type DSLActivityExecutionPayload } from '@vertesia/common';
|
|
4
|
-
import type { ActivityContext } from '@vertesia/workflow';
|
|
5
4
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { ActivityContext } from '../../dsl/setup/ActivityContext.js';
|
|
6
6
|
import { type ProbeMediaStreamsParams, type ProbeMediaStreamsResult, probeMediaStreams } from './probeMediaStreams.js';
|
|
7
7
|
|
|
8
8
|
vi.mock('../../dsl/setup/ActivityContext.js', async (importOriginal) => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { MockActivityEnvironment } from '@temporalio/testing';
|
|
2
2
|
import type { VertesiaClient } from '@vertesia/client';
|
|
3
3
|
import { ContentEventName, type DSLActivityExecutionPayload } from '@vertesia/common';
|
|
4
|
-
import type { ActivityContext, TextExtractionResult } from '@vertesia/workflow';
|
|
5
4
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { ActivityContext } from '../../dsl/setup/ActivityContext.js';
|
|
6
|
+
import type { TextExtractionResult } from '../../result-types.js';
|
|
6
7
|
import { TextExtractionStatus } from '../../result-types.js';
|
|
7
8
|
import { type SaveGladiaTranscriptionParams, saveGladiaTranscription } from './saveGladiaTranscription.js';
|
|
8
9
|
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
ContentNature,
|
|
8
8
|
type DSLActivityExecutionPayload,
|
|
9
9
|
} from '@vertesia/common';
|
|
10
|
-
import type { ActivityContext } from '@vertesia/workflow';
|
|
11
10
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import type { ActivityContext } from '../../dsl/setup/ActivityContext.js';
|
|
12
12
|
import { TextExtractionStatus } from '../../result-types.js';
|
|
13
13
|
import {
|
|
14
14
|
type TranscriptMediaParams,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MockActivityEnvironment } from '@temporalio/testing';
|
|
2
|
+
import type { VertesiaClient } from '@vertesia/client';
|
|
2
3
|
import { ApiVersions, ContentEventName, type DSLActivityExecutionPayload, type WebHookSpec } from '@vertesia/common';
|
|
3
4
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import {
|
|
@@ -8,6 +9,23 @@ import {
|
|
|
8
9
|
type WebhookNotificationPayload,
|
|
9
10
|
} from './notifyWebhook.js';
|
|
10
11
|
|
|
12
|
+
const mockValidateUrl = vi.hoisted(() => vi.fn());
|
|
13
|
+
|
|
14
|
+
vi.mock('../utils/client.js', async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal<typeof import('../utils/client.js')>();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
getVertesiaClient: vi.fn(
|
|
19
|
+
() =>
|
|
20
|
+
({
|
|
21
|
+
apps: {
|
|
22
|
+
validateUrl: mockValidateUrl,
|
|
23
|
+
},
|
|
24
|
+
}) as unknown as VertesiaClient,
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
11
29
|
// Mock fetch globally
|
|
12
30
|
vi.stubGlobal('fetch', vi.fn());
|
|
13
31
|
|
|
@@ -20,6 +38,7 @@ beforeAll(async () => {
|
|
|
20
38
|
|
|
21
39
|
beforeEach(() => {
|
|
22
40
|
vi.clearAllMocks();
|
|
41
|
+
mockValidateUrl.mockResolvedValue({ valid: true });
|
|
23
42
|
});
|
|
24
43
|
|
|
25
44
|
const defaultParams = {
|
|
@@ -64,6 +83,7 @@ describe('Webhook should be notified', () => {
|
|
|
64
83
|
status: 200,
|
|
65
84
|
statusText: 'OK',
|
|
66
85
|
url: defaultParams.webhook,
|
|
86
|
+
headers: new Headers(),
|
|
67
87
|
};
|
|
68
88
|
mockFetch.mockResolvedValueOnce(mockResponse as Response);
|
|
69
89
|
|
|
@@ -82,6 +102,7 @@ describe('Webhook should be notified', () => {
|
|
|
82
102
|
headers: {
|
|
83
103
|
'Content-Type': 'application/json',
|
|
84
104
|
},
|
|
105
|
+
redirect: 'manual',
|
|
85
106
|
});
|
|
86
107
|
|
|
87
108
|
// Verify response
|
|
@@ -99,6 +120,7 @@ describe('Webhook should be notified', () => {
|
|
|
99
120
|
status: 500,
|
|
100
121
|
statusText: 'Internal Server Error',
|
|
101
122
|
url: defaultParams.webhook,
|
|
123
|
+
headers: new Headers(),
|
|
102
124
|
text: vi.fn().mockResolvedValue('{"error": "Database connection failed", "code": "DB_ERROR"}'),
|
|
103
125
|
} as unknown as Response;
|
|
104
126
|
mockFetch.mockResolvedValueOnce(mockResponse);
|
|
@@ -122,6 +144,7 @@ describe('Webhook should be notified', () => {
|
|
|
122
144
|
headers: {
|
|
123
145
|
'Content-Type': 'application/json',
|
|
124
146
|
},
|
|
147
|
+
redirect: 'manual',
|
|
125
148
|
});
|
|
126
149
|
|
|
127
150
|
// Verify that text() was called to read the response
|
|
@@ -150,6 +173,100 @@ describe('Webhook should be notified', () => {
|
|
|
150
173
|
headers: {
|
|
151
174
|
'Content-Type': 'application/json',
|
|
152
175
|
},
|
|
176
|
+
redirect: 'manual',
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should block URLs rejected by central URL validation', async () => {
|
|
181
|
+
mockValidateUrl.mockRejectedValueOnce(new Error('Access to internal hosts is not allowed'));
|
|
182
|
+
|
|
183
|
+
const payload = createTestPayload({
|
|
184
|
+
webhook: 'https://metadata.google.internal/computeMetadata/v1/',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await expect(testEnv.run(notifyWebhook, payload)).rejects.toThrow(
|
|
188
|
+
'Webhook endpoint blocked: Access to internal hosts is not allowed',
|
|
189
|
+
);
|
|
190
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should retry transient central URL validation failures', async () => {
|
|
194
|
+
const transientError = Object.assign(new Error('studio-server unavailable'), { statusCode: 503 });
|
|
195
|
+
mockValidateUrl.mockRejectedValueOnce(transientError);
|
|
196
|
+
|
|
197
|
+
const payload = createTestPayload();
|
|
198
|
+
|
|
199
|
+
await expect(testEnv.run(notifyWebhook, payload)).rejects.toThrow('studio-server unavailable');
|
|
200
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should block webhook redirects', async () => {
|
|
204
|
+
mockFetch.mockResolvedValueOnce(
|
|
205
|
+
new Response(null, {
|
|
206
|
+
status: 302,
|
|
207
|
+
statusText: 'Found',
|
|
208
|
+
headers: {
|
|
209
|
+
Location: 'https://example.com/redirected',
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const payload = createTestPayload();
|
|
215
|
+
|
|
216
|
+
await expect(testEnv.run(notifyWebhook, payload)).rejects.toThrow('Webhook endpoint blocked: Request to');
|
|
217
|
+
expect(mockFetch).toHaveBeenCalledWith(defaultParams.webhook, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
workflowId: 'wf_id',
|
|
221
|
+
runId: 'wf_run_id',
|
|
222
|
+
status: 'completed',
|
|
223
|
+
result: { message: 'Hello World' },
|
|
224
|
+
}),
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/json',
|
|
227
|
+
},
|
|
228
|
+
redirect: 'manual',
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should send a raw body when provided', async () => {
|
|
233
|
+
const mockResponse = {
|
|
234
|
+
ok: true,
|
|
235
|
+
status: 202,
|
|
236
|
+
statusText: 'Accepted',
|
|
237
|
+
url: defaultParams.webhook,
|
|
238
|
+
headers: new Headers(),
|
|
239
|
+
};
|
|
240
|
+
mockFetch.mockResolvedValueOnce(mockResponse as Response);
|
|
241
|
+
|
|
242
|
+
const body = JSON.stringify({ event: { event_id: 'evt-1' } });
|
|
243
|
+
const payload = createTestPayload({
|
|
244
|
+
body,
|
|
245
|
+
headers: {
|
|
246
|
+
'content-type': 'application/json',
|
|
247
|
+
'X-Vertesia-Event-Id': 'evt-1',
|
|
248
|
+
},
|
|
249
|
+
timeout_ms: 5000,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const res: NotifyWebhookResult = await testEnv.run(notifyWebhook, payload);
|
|
253
|
+
|
|
254
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
255
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
256
|
+
expect(url).toBe(defaultParams.webhook);
|
|
257
|
+
expect(options).toMatchObject({
|
|
258
|
+
method: 'POST',
|
|
259
|
+
body,
|
|
260
|
+
headers: {
|
|
261
|
+
'content-type': 'application/json',
|
|
262
|
+
'X-Vertesia-Event-Id': 'evt-1',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
expect(options?.signal).toBeInstanceOf(AbortSignal);
|
|
266
|
+
expect(res).toEqual({
|
|
267
|
+
status: 202,
|
|
268
|
+
message: 'Accepted',
|
|
269
|
+
url: defaultParams.webhook,
|
|
153
270
|
});
|
|
154
271
|
});
|
|
155
272
|
|
|
@@ -160,6 +277,7 @@ describe('Webhook should be notified', () => {
|
|
|
160
277
|
status: 200,
|
|
161
278
|
statusText: 'OK',
|
|
162
279
|
url: 'https://vertesia.test',
|
|
280
|
+
headers: new Headers(),
|
|
163
281
|
};
|
|
164
282
|
mockFetch.mockResolvedValueOnce(mockResponse as Response);
|
|
165
283
|
|
|
@@ -213,6 +331,7 @@ describe('Webhook should be notified', () => {
|
|
|
213
331
|
status: 200,
|
|
214
332
|
statusText: 'OK',
|
|
215
333
|
url: 'https://vertesia.test',
|
|
334
|
+
headers: new Headers(),
|
|
216
335
|
};
|
|
217
336
|
mockFetch.mockResolvedValueOnce(mockResponse as Response);
|
|
218
337
|
|