@vertesia/workflow 1.0.0-dev.20260305.083323Z → 1.0.0
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/cjs/activities/chunkDocument.js +1 -3
- package/lib/cjs/activities/chunkDocument.js.map +1 -1
- package/lib/cjs/activities/executeInteraction.js +18 -0
- package/lib/cjs/activities/executeInteraction.js.map +1 -1
- package/lib/cjs/activities/executeRemoteActivity.js +107 -0
- package/lib/cjs/activities/executeRemoteActivity.js.map +1 -0
- package/lib/cjs/activities/getObjectFromStore.js +12 -1
- package/lib/cjs/activities/getObjectFromStore.js.map +1 -1
- package/lib/cjs/activities/index-dsl.js +5 -3
- package/lib/cjs/activities/index-dsl.js.map +1 -1
- package/lib/cjs/activities/resolveRemoteActivities.js +120 -0
- package/lib/cjs/activities/resolveRemoteActivities.js.map +1 -0
- package/lib/cjs/dsl/dsl-workflow.js +66 -6
- package/lib/cjs/dsl/dsl-workflow.js.map +1 -1
- package/lib/cjs/utils/storage.js +9 -9
- package/lib/cjs/utils/storage.js.map +1 -1
- package/lib/esm/activities/chunkDocument.js +1 -3
- package/lib/esm/activities/chunkDocument.js.map +1 -1
- package/lib/esm/activities/executeInteraction.js +18 -0
- package/lib/esm/activities/executeInteraction.js.map +1 -1
- package/lib/esm/activities/executeRemoteActivity.js +104 -0
- package/lib/esm/activities/executeRemoteActivity.js.map +1 -0
- package/lib/esm/activities/getObjectFromStore.js +12 -1
- package/lib/esm/activities/getObjectFromStore.js.map +1 -1
- package/lib/esm/activities/index-dsl.js +2 -1
- package/lib/esm/activities/index-dsl.js.map +1 -1
- package/lib/esm/activities/resolveRemoteActivities.js +117 -0
- package/lib/esm/activities/resolveRemoteActivities.js.map +1 -0
- package/lib/esm/dsl/dsl-workflow.js +66 -6
- package/lib/esm/dsl/dsl-workflow.js.map +1 -1
- package/lib/esm/utils/storage.js +9 -9
- package/lib/esm/utils/storage.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/activities/chunkDocument.d.ts.map +1 -1
- package/lib/types/activities/executeInteraction.d.ts.map +1 -1
- package/lib/types/activities/executeRemoteActivity.d.ts +31 -0
- package/lib/types/activities/executeRemoteActivity.d.ts.map +1 -0
- package/lib/types/activities/getObjectFromStore.d.ts.map +1 -1
- package/lib/types/activities/index-dsl.d.ts +3 -1
- package/lib/types/activities/index-dsl.d.ts.map +1 -1
- package/lib/types/activities/resolveRemoteActivities.d.ts +32 -0
- package/lib/types/activities/resolveRemoteActivities.d.ts.map +1 -0
- package/lib/types/dsl/dsl-workflow.d.ts.map +1 -1
- package/lib/types/utils/storage.d.ts +2 -2
- package/lib/types/utils/storage.d.ts.map +1 -1
- package/lib/workflows-bundle.js +625 -274
- package/package.json +6 -6
- package/src/activities/chunkDocument.ts +1 -3
- package/src/activities/executeInteraction.ts +17 -0
- package/src/activities/executeRemoteActivity.test.ts +140 -0
- package/src/activities/executeRemoteActivity.ts +133 -0
- package/src/activities/getObjectFromStore.ts +11 -1
- package/src/activities/index-dsl.ts +3 -1
- package/src/activities/resolveRemoteActivities.test.ts +220 -0
- package/src/activities/resolveRemoteActivities.ts +167 -0
- package/src/dsl/dsl-workflow.ts +87 -7
- package/src/utils/storage.ts +9 -8
- package/lib/cjs/activities/copyParentArtifacts.js +0 -127
- package/lib/cjs/activities/copyParentArtifacts.js.map +0 -1
- package/lib/esm/activities/copyParentArtifacts.js +0 -124
- package/lib/esm/activities/copyParentArtifacts.js.map +0 -1
- package/lib/types/activities/copyParentArtifacts.d.ts +0 -19
- package/lib/types/activities/copyParentArtifacts.d.ts.map +0 -1
- package/src/activities/copyParentArtifacts.ts +0 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/workflow",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vertesia workflow DSL",
|
|
6
6
|
"main": "./lib/esm/index.js",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"tmp": "^0.2.4",
|
|
44
44
|
"tmp-promise": "^3.0.3",
|
|
45
45
|
"yaml": "^2.6.0",
|
|
46
|
-
"@llumiverse/common": "1.0.0
|
|
47
|
-
"@vertesia/
|
|
48
|
-
"@vertesia/client": "1.0.0
|
|
49
|
-
"@vertesia/common": "1.0.0
|
|
50
|
-
"@vertesia/memory": "1.0.0
|
|
46
|
+
"@llumiverse/common": "1.0.0",
|
|
47
|
+
"@vertesia/client": "1.0.0",
|
|
48
|
+
"@vertesia/api-fetch-client": "1.0.0",
|
|
49
|
+
"@vertesia/common": "1.0.0",
|
|
50
|
+
"@vertesia/memory": "1.0.0"
|
|
51
51
|
},
|
|
52
52
|
"ts_dual_module": {
|
|
53
53
|
"outDir": "lib",
|
|
@@ -157,9 +157,7 @@ export async function chunkDocument(payload: DSLActivityExecutionPayload<ChunkDo
|
|
|
157
157
|
//delete previous parts
|
|
158
158
|
if (document.parts && document.parts.length > 0) {
|
|
159
159
|
log.info('Deleting previous parts for object ID: ' + objectId, { parts: document.parts });
|
|
160
|
-
await
|
|
161
|
-
await client.objects.delete(partId);
|
|
162
|
-
}));
|
|
160
|
+
await client.objects.delete(document.parts);
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
await client.objects.update(objectId, {
|
|
@@ -202,6 +202,14 @@ export async function executeInteraction(payload: DSLActivityExecutionPayload<Ex
|
|
|
202
202
|
log.error(`Failed to execute interaction ${interactionName}`, { error });
|
|
203
203
|
if (error.statusCode === 429 && params.exit_on_resource_exhaustion) {
|
|
204
204
|
throw new ResourceExhaustedError(error.statusCode, "Resource exhausted - rate limit exceeded");
|
|
205
|
+
} else if (is4xxNonRetryable(error.status) || is4xxNonRetryable(error.statusCode) || is4xxNonRetryable(error.code) || error.retryable === false) {
|
|
206
|
+
// 4xx HTTP errors (except 429 rate-limit) are permanent client errors (e.g. model not found, invalid request).
|
|
207
|
+
// Errors explicitly marked as non-retryable (e.g. LlumiverseError) also fall here.
|
|
208
|
+
// They will not be resolved by retrying.
|
|
209
|
+
throw ApplicationFailure.create({
|
|
210
|
+
message: `Interaction Execution failed ${interactionName}: ${error.message}`,
|
|
211
|
+
nonRetryable: true,
|
|
212
|
+
});
|
|
205
213
|
} else if (error.message.includes("Failed to validate merged prompt schema")) {
|
|
206
214
|
//issue with the input data, don't retry
|
|
207
215
|
throw new ActivityParamInvalidError("prompt_data", payload.activity, error.message);
|
|
@@ -330,3 +338,12 @@ export async function executeInteractionFromActivity(
|
|
|
330
338
|
|
|
331
339
|
return res;
|
|
332
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Returns true for 4xx status codes that indicate permanent client errors.
|
|
344
|
+
* 429 (Too Many Requests) is excluded because it is retryable.
|
|
345
|
+
*/
|
|
346
|
+
function is4xxNonRetryable(code: number | undefined): boolean {
|
|
347
|
+
if (code === undefined || typeof code !== 'number') return false;
|
|
348
|
+
return code >= 400 && code < 500 && code !== 429;
|
|
349
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { MockActivityEnvironment } from "@temporalio/testing";
|
|
2
|
+
import { ContentEventName, DSLActivityExecutionPayload } from "@vertesia/common";
|
|
3
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { executeRemoteActivity, ExecuteRemoteActivityParams } from "./executeRemoteActivity.js";
|
|
5
|
+
|
|
6
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
7
|
+
|
|
8
|
+
let testEnv: MockActivityEnvironment;
|
|
9
|
+
const mockFetch = vi.mocked(fetch);
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
testEnv = new MockActivityEnvironment();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const createPayload = (
|
|
20
|
+
overrides: Partial<ExecuteRemoteActivityParams> = {},
|
|
21
|
+
): DSLActivityExecutionPayload<ExecuteRemoteActivityParams> => {
|
|
22
|
+
const params: ExecuteRemoteActivityParams = {
|
|
23
|
+
url: "https://tool-server.test/api/activities/nlp",
|
|
24
|
+
activity_name: "analyze_sentiment",
|
|
25
|
+
params: { text: "Hello world" },
|
|
26
|
+
app_install_id: "install-123",
|
|
27
|
+
app_name: "nlp-app",
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
auth_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbW9jay10b2tlbi1zZXJ2ZXIiLCJzdWIiOiJ0ZXN0In0.sig",
|
|
32
|
+
account_id: "acc-123",
|
|
33
|
+
project_id: "proj-456",
|
|
34
|
+
params,
|
|
35
|
+
config: {
|
|
36
|
+
studio_url: "http://mock-studio",
|
|
37
|
+
store_url: "http://mock-store",
|
|
38
|
+
},
|
|
39
|
+
workflow_name: "TestWorkflow",
|
|
40
|
+
event: ContentEventName.create,
|
|
41
|
+
objectIds: [],
|
|
42
|
+
vars: {},
|
|
43
|
+
activity: { name: "executeRemoteActivity", params },
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe("executeRemoteActivity", () => {
|
|
48
|
+
it("posts correct payload and returns result on success", async () => {
|
|
49
|
+
mockFetch.mockResolvedValueOnce(
|
|
50
|
+
new Response(
|
|
51
|
+
JSON.stringify({ result: { score: 0.95 }, is_error: false }),
|
|
52
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const result = await testEnv.run(executeRemoteActivity, createPayload());
|
|
57
|
+
expect(result).toEqual({ score: 0.95 });
|
|
58
|
+
|
|
59
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
60
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
61
|
+
expect(url).toBe("https://tool-server.test/api/activities/nlp");
|
|
62
|
+
expect(opts?.method).toBe("POST");
|
|
63
|
+
expect(opts?.headers).toMatchObject({
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
Accept: "application/json",
|
|
66
|
+
});
|
|
67
|
+
// Verify the auth header is forwarded
|
|
68
|
+
expect((opts?.headers as Record<string, string>)["Authorization"]).toMatch(/^Bearer /);
|
|
69
|
+
|
|
70
|
+
// Verify the body structure
|
|
71
|
+
const body = JSON.parse(opts?.body as string);
|
|
72
|
+
expect(body.activity_name).toBe("analyze_sentiment");
|
|
73
|
+
expect(body.params).toEqual({ text: "Hello world" });
|
|
74
|
+
expect(body.metadata.app_install_id).toBe("install-123");
|
|
75
|
+
expect(body.metadata.endpoints).toEqual({ studio: "http://mock-studio", store: "http://mock-store" });
|
|
76
|
+
// auth_token should NOT be in the payload (it's in the Authorization header)
|
|
77
|
+
expect(body.auth_token).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("throws on HTTP error (4xx/5xx)", async () => {
|
|
81
|
+
mockFetch.mockResolvedValueOnce(
|
|
82
|
+
new Response(
|
|
83
|
+
JSON.stringify({ error: "Activity not found", is_error: true }),
|
|
84
|
+
{ status: 404, statusText: "Not Found" },
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await expect(testEnv.run(executeRemoteActivity, createPayload())).rejects.toThrow(
|
|
89
|
+
"Remote activity analyze_sentiment failed: Activity not found",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("throws on network error (for Temporal retry)", async () => {
|
|
94
|
+
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
|
|
95
|
+
|
|
96
|
+
await expect(testEnv.run(executeRemoteActivity, createPayload())).rejects.toThrow(
|
|
97
|
+
/Failed to reach remote activity endpoint/,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("throws on invalid JSON response", async () => {
|
|
102
|
+
mockFetch.mockResolvedValueOnce(
|
|
103
|
+
new Response("not json", { status: 200 }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await expect(testEnv.run(executeRemoteActivity, createPayload())).rejects.toThrow(
|
|
107
|
+
/returned invalid JSON/,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("throws when response indicates is_error", async () => {
|
|
112
|
+
mockFetch.mockResolvedValueOnce(
|
|
113
|
+
new Response(
|
|
114
|
+
JSON.stringify({ result: null, is_error: true, error: "Something went wrong" }),
|
|
115
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await expect(testEnv.run(executeRemoteActivity, createPayload())).rejects.toThrow(
|
|
120
|
+
"Remote activity analyze_sentiment: Something went wrong",
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("forwards app_settings in metadata", async () => {
|
|
125
|
+
mockFetch.mockResolvedValueOnce(
|
|
126
|
+
new Response(
|
|
127
|
+
JSON.stringify({ result: "ok", is_error: false }),
|
|
128
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await testEnv.run(
|
|
133
|
+
executeRemoteActivity,
|
|
134
|
+
createPayload({ app_settings: { api_key: "secret" } }),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1]?.body as string);
|
|
138
|
+
expect(body.metadata.app_settings).toEqual({ api_key: "secret" });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ApplicationFailure, log } from "@temporalio/activity";
|
|
2
|
+
import {
|
|
3
|
+
DSLActivityExecutionPayload,
|
|
4
|
+
RemoteActivityExecutionPayload,
|
|
5
|
+
RemoteActivityExecutionResponse,
|
|
6
|
+
} from "@vertesia/common";
|
|
7
|
+
import { setupActivity } from "../dsl/setup/ActivityContext.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parameters for the executeRemoteActivity bridge activity.
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecuteRemoteActivityParams {
|
|
13
|
+
/** URL of the remote activity endpoint on the tool server */
|
|
14
|
+
url: string;
|
|
15
|
+
/** The activity name (unprefixed, as known by the tool server) */
|
|
16
|
+
activity_name: string;
|
|
17
|
+
/** The resolved parameters for the activity */
|
|
18
|
+
params: Record<string, any>;
|
|
19
|
+
/** App installation ID */
|
|
20
|
+
app_install_id: string;
|
|
21
|
+
/** App name */
|
|
22
|
+
app_name: string;
|
|
23
|
+
/** App installation settings */
|
|
24
|
+
app_settings?: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bridge activity that executes a remote activity on a tool server via HTTP POST.
|
|
29
|
+
*
|
|
30
|
+
* This activity is called by the DSL workflow engine when a step's name matches
|
|
31
|
+
* a remote activity (qualified as `app:<app_name>:<collection>:<activity>`). It POSTs a
|
|
32
|
+
* `RemoteActivityExecutionPayload` to the tool server and returns the result.
|
|
33
|
+
*
|
|
34
|
+
* Network errors throw retryable errors (so Temporal retries).
|
|
35
|
+
* Client errors (4xx) and activity-level errors throw non-retryable ApplicationFailure.
|
|
36
|
+
* Server errors (5xx) throw retryable errors.
|
|
37
|
+
*/
|
|
38
|
+
export async function executeRemoteActivity(
|
|
39
|
+
payload: DSLActivityExecutionPayload<ExecuteRemoteActivityParams>,
|
|
40
|
+
): Promise<any> {
|
|
41
|
+
const ctx = await setupActivity<ExecuteRemoteActivityParams>(payload);
|
|
42
|
+
const { params, runId } = ctx;
|
|
43
|
+
const { url, activity_name, params: activityParams, app_install_id, app_settings } = params;
|
|
44
|
+
|
|
45
|
+
const executionPayload: RemoteActivityExecutionPayload = {
|
|
46
|
+
activity_name,
|
|
47
|
+
params: activityParams,
|
|
48
|
+
metadata: {
|
|
49
|
+
run_id: runId,
|
|
50
|
+
app_install_id,
|
|
51
|
+
app_settings,
|
|
52
|
+
endpoints: payload.config ? {
|
|
53
|
+
studio: payload.config.studio_url,
|
|
54
|
+
store: payload.config.store_url,
|
|
55
|
+
} : undefined,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let response: Response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(url, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Accept": "application/json",
|
|
66
|
+
"Authorization": `Bearer ${payload.auth_token}`,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(executionPayload),
|
|
69
|
+
});
|
|
70
|
+
} catch (err: unknown) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
log.warn("Failed to reach remote activity endpoint", {
|
|
73
|
+
error: message, activity: activity_name, endpoint: url, runId, app_install_id,
|
|
74
|
+
});
|
|
75
|
+
// Network-level failure — let Temporal retry
|
|
76
|
+
throw new Error(`Failed to reach remote activity endpoint (activity: ${activity_name}, endpoint: ${url}): ${message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const responseText = await response.text();
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
let errorMessage = `HTTP ${response.status} ${response.statusText}`;
|
|
83
|
+
try {
|
|
84
|
+
const errorJson = JSON.parse(responseText) as RemoteActivityExecutionResponse;
|
|
85
|
+
if (errorJson.error) {
|
|
86
|
+
errorMessage = errorJson.error;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Not JSON — use the status line
|
|
90
|
+
}
|
|
91
|
+
log.warn("Remote activity returned HTTP error", {
|
|
92
|
+
activity: activity_name, endpoint: url, status: response.status, runId, app_install_id,
|
|
93
|
+
responsePreview: responseText.slice(0, 500),
|
|
94
|
+
});
|
|
95
|
+
const msg = `Remote activity ${activity_name} failed: ${errorMessage}`;
|
|
96
|
+
// 4xx errors are client/configuration errors — don't retry
|
|
97
|
+
if (response.status >= 400 && response.status < 500) {
|
|
98
|
+
throw ApplicationFailure.create({
|
|
99
|
+
message: msg,
|
|
100
|
+
nonRetryable: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// 5xx errors are server errors — let Temporal retry
|
|
104
|
+
throw new Error(msg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let responseJson: RemoteActivityExecutionResponse;
|
|
108
|
+
try {
|
|
109
|
+
responseJson = JSON.parse(responseText);
|
|
110
|
+
} catch (err: unknown) {
|
|
111
|
+
const preview = responseText.length > 200 ? responseText.slice(0, 200) + '...' : responseText;
|
|
112
|
+
log.warn("Invalid JSON response from remote activity", {
|
|
113
|
+
activity: activity_name, endpoint: url, runId, app_install_id,
|
|
114
|
+
responsePreview: preview,
|
|
115
|
+
});
|
|
116
|
+
throw ApplicationFailure.create({
|
|
117
|
+
message: `Remote activity ${activity_name} returned invalid JSON: ${preview}`,
|
|
118
|
+
nonRetryable: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (responseJson.is_error) {
|
|
123
|
+
log.warn("Remote activity returned error", {
|
|
124
|
+
activity: activity_name, endpoint: url, error: responseJson.error, runId, app_install_id,
|
|
125
|
+
});
|
|
126
|
+
throw ApplicationFailure.create({
|
|
127
|
+
message: `Remote activity ${activity_name}: ${responseJson.error}`,
|
|
128
|
+
nonRetryable: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return responseJson.result;
|
|
133
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ContentObject, DSLActivityExecutionPayload, DSLActivitySpec } from "@vertesia/common";
|
|
2
|
+
import { DocumentNotFoundError } from "../errors.js";
|
|
2
3
|
import { projectResult } from "../dsl/projections.js";
|
|
3
4
|
import { setupActivity } from "../dsl/setup/ActivityContext.js";
|
|
4
5
|
|
|
@@ -19,7 +20,16 @@ export interface GetObject extends DSLActivitySpec<GetObjectParams> {
|
|
|
19
20
|
export async function getObjectFromStore(payload: DSLActivityExecutionPayload<GetObjectParams>): Promise<ContentObject> {
|
|
20
21
|
const { client, params, objectId } = await setupActivity<GetObjectParams>(payload);
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
let obj: ContentObject;
|
|
24
|
+
try {
|
|
25
|
+
obj = await client.objects.retrieve(objectId, params.select);
|
|
26
|
+
} catch (err: unknown) {
|
|
27
|
+
const status = err && typeof err === 'object' && 'status' in err ? (err as { status: number }).status : 0;
|
|
28
|
+
if (status >= 400 && status < 500 && status !== 429) {
|
|
29
|
+
throw new DocumentNotFoundError(`Object retrieval failed (${status}): ${objectId}`, [objectId]);
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
const projection = projectResult(payload, params, obj, obj);
|
|
25
35
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Export here DSL activities
|
|
2
|
+
export { executeRemoteActivity } from "./executeRemoteActivity.js";
|
|
3
|
+
export { resolveRemoteActivities } from "./resolveRemoteActivities.js";
|
|
4
|
+
export type { RemoteActivityInfo, RemoteActivityMap } from "./resolveRemoteActivities.js";
|
|
2
5
|
export { createDocumentTypeFromInteractionRun } from "./advanced/createDocumentTypeFromInteractionRun.js";
|
|
3
6
|
export { createOrUpdateDocumentFromInteractionRun } from "./advanced/createOrUpdateDocumentFromInteractionRun.js";
|
|
4
7
|
export { updateDocumentFromInteractionRun } from "./advanced/updateDocumentFromInteractionRun.js";
|
|
5
8
|
export { chunkDocument } from "./chunkDocument.js";
|
|
6
|
-
export { copyParentArtifacts } from "./copyParentArtifacts.js";
|
|
7
9
|
export { createPdfDocumentFromSource } from "./createDocumentFromOther.js";
|
|
8
10
|
export { executeInteraction } from "./executeInteraction.js";
|
|
9
11
|
export { extractDocumentText } from "./extractDocumentText.js";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { MockActivityEnvironment } from "@temporalio/testing";
|
|
2
|
+
import { ContentEventName, DSLActivityExecutionPayload } from "@vertesia/common";
|
|
3
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { resolveRemoteActivities, ResolveRemoteActivitiesParams } from "./resolveRemoteActivities.js";
|
|
5
|
+
|
|
6
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
7
|
+
|
|
8
|
+
// Mock getVertesiaClient
|
|
9
|
+
const mockGetInstalledApps = vi.fn();
|
|
10
|
+
vi.mock("../utils/client.js", () => ({
|
|
11
|
+
getVertesiaClient: vi.fn().mockReturnValue({
|
|
12
|
+
apps: {
|
|
13
|
+
getInstalledApps: (...args: any[]) => mockGetInstalledApps(...args),
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
let testEnv: MockActivityEnvironment;
|
|
19
|
+
const mockFetch = vi.mocked(fetch);
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
testEnv = new MockActivityEnvironment();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createPayload = (): DSLActivityExecutionPayload<ResolveRemoteActivitiesParams> => ({
|
|
30
|
+
auth_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbW9jay10b2tlbi1zZXJ2ZXIiLCJzdWIiOiJ0ZXN0In0.sig",
|
|
31
|
+
account_id: "acc-123",
|
|
32
|
+
project_id: "proj-456",
|
|
33
|
+
params: {},
|
|
34
|
+
config: {
|
|
35
|
+
studio_url: "http://mock-studio",
|
|
36
|
+
store_url: "http://mock-store",
|
|
37
|
+
},
|
|
38
|
+
workflow_name: "TestWorkflow",
|
|
39
|
+
event: ContentEventName.create,
|
|
40
|
+
objectIds: [],
|
|
41
|
+
vars: {},
|
|
42
|
+
activity: { name: "resolveRemoteActivities", params: {} },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("resolveRemoteActivities", () => {
|
|
46
|
+
it("returns empty map when no apps installed", async () => {
|
|
47
|
+
mockGetInstalledApps.mockResolvedValueOnce([]);
|
|
48
|
+
|
|
49
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
50
|
+
expect(result).toEqual({});
|
|
51
|
+
expect(mockGetInstalledApps).toHaveBeenCalledWith("tools");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns qualified activity map from single app", async () => {
|
|
55
|
+
mockGetInstalledApps.mockResolvedValueOnce([{
|
|
56
|
+
id: "install-1",
|
|
57
|
+
manifest: {
|
|
58
|
+
name: "my-nlp-app",
|
|
59
|
+
endpoint: "https://nlp-server.test/api/package",
|
|
60
|
+
},
|
|
61
|
+
settings: { api_key: "test" },
|
|
62
|
+
}]);
|
|
63
|
+
|
|
64
|
+
mockFetch.mockResolvedValueOnce(
|
|
65
|
+
new Response(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
activities: [
|
|
68
|
+
{ name: "analyze_sentiment", description: "Analyze sentiment", collection: "nlp" },
|
|
69
|
+
{ name: "extract_entities", description: "Extract entities", collection: "nlp" },
|
|
70
|
+
],
|
|
71
|
+
}),
|
|
72
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
77
|
+
|
|
78
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
79
|
+
expect(result["app:my-nlp-app:nlp:analyze_sentiment"]).toBeDefined();
|
|
80
|
+
expect(result["app:my-nlp-app:nlp:extract_entities"]).toBeDefined();
|
|
81
|
+
|
|
82
|
+
const entry = result["app:my-nlp-app:nlp:analyze_sentiment"];
|
|
83
|
+
expect(entry.activity_name).toBe("analyze_sentiment");
|
|
84
|
+
expect(entry.app_name).toBe("my-nlp-app");
|
|
85
|
+
expect(entry.app_install_id).toBe("install-1");
|
|
86
|
+
expect(entry.app_settings).toEqual({ api_key: "test" });
|
|
87
|
+
// URL should target the collection-specific endpoint
|
|
88
|
+
expect(entry.url).toBe("https://nlp-server.test/api/activities/nlp");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("merges activities from multiple apps", async () => {
|
|
92
|
+
mockGetInstalledApps.mockResolvedValueOnce([
|
|
93
|
+
{
|
|
94
|
+
id: "install-1",
|
|
95
|
+
manifest: { name: "app-one", endpoint: "https://one.test/api/package" },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "install-2",
|
|
99
|
+
manifest: { name: "app-two", endpoint: "https://two.test/api/package" },
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
mockFetch
|
|
104
|
+
.mockResolvedValueOnce(
|
|
105
|
+
new Response(JSON.stringify({ activities: [{ name: "task_a", collection: "main" }] }), { status: 200 }),
|
|
106
|
+
)
|
|
107
|
+
.mockResolvedValueOnce(
|
|
108
|
+
new Response(JSON.stringify({ activities: [{ name: "task_b", collection: "main" }] }), { status: 200 }),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
112
|
+
|
|
113
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
114
|
+
expect(result["app:app-one:main:task_a"]).toBeDefined();
|
|
115
|
+
expect(result["app:app-two:main:task_b"]).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("skips app with no activities", async () => {
|
|
119
|
+
mockGetInstalledApps.mockResolvedValueOnce([{
|
|
120
|
+
id: "install-1",
|
|
121
|
+
manifest: { name: "empty-app", endpoint: "https://empty.test/api/package" },
|
|
122
|
+
}]);
|
|
123
|
+
|
|
124
|
+
mockFetch.mockResolvedValueOnce(
|
|
125
|
+
new Response(JSON.stringify({ activities: [] }), { status: 200 }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
129
|
+
expect(result).toEqual({});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("skips app with no endpoint", async () => {
|
|
133
|
+
mockGetInstalledApps.mockResolvedValueOnce([{
|
|
134
|
+
id: "install-1",
|
|
135
|
+
manifest: { name: "no-endpoint" },
|
|
136
|
+
}]);
|
|
137
|
+
|
|
138
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
139
|
+
expect(result).toEqual({});
|
|
140
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("handles duplicate qualified names across apps (first wins)", async () => {
|
|
144
|
+
mockGetInstalledApps.mockResolvedValueOnce([
|
|
145
|
+
{
|
|
146
|
+
id: "install-1",
|
|
147
|
+
manifest: { name: "same-app", endpoint: "https://one.test/api/package" },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "install-2",
|
|
151
|
+
manifest: { name: "same-app", endpoint: "https://two.test/api/package" },
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
mockFetch
|
|
156
|
+
.mockResolvedValueOnce(
|
|
157
|
+
new Response(JSON.stringify({ activities: [{ name: "task", collection: "main" }] }), { status: 200 }),
|
|
158
|
+
)
|
|
159
|
+
.mockResolvedValueOnce(
|
|
160
|
+
new Response(JSON.stringify({ activities: [{ name: "task", collection: "main" }] }), { status: 200 }),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
164
|
+
expect(Object.keys(result)).toHaveLength(1);
|
|
165
|
+
expect(result["app:same-app:main:task"].app_install_id).toBe("install-1");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("continues with other apps when one fetch fails", async () => {
|
|
169
|
+
mockGetInstalledApps.mockResolvedValueOnce([
|
|
170
|
+
{
|
|
171
|
+
id: "install-1",
|
|
172
|
+
manifest: { name: "failing-app", endpoint: "https://fail.test/api/package" },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "install-2",
|
|
176
|
+
manifest: { name: "working-app", endpoint: "https://work.test/api/package" },
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
mockFetch
|
|
181
|
+
.mockRejectedValueOnce(new Error("Connection refused"))
|
|
182
|
+
.mockResolvedValueOnce(
|
|
183
|
+
new Response(JSON.stringify({ activities: [{ name: "task", collection: "main" }] }), { status: 200 }),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
187
|
+
expect(Object.keys(result)).toHaveLength(1);
|
|
188
|
+
expect(result["app:working-app:main:task"]).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns empty map when getInstalledApps fails", async () => {
|
|
192
|
+
mockGetInstalledApps.mockRejectedValueOnce(new Error("API error"));
|
|
193
|
+
|
|
194
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload());
|
|
195
|
+
expect(result).toEqual({});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("skips activities without collection", async () => {
|
|
199
|
+
mockGetInstalledApps.mockResolvedValueOnce([{
|
|
200
|
+
id: "install-1",
|
|
201
|
+
manifest: { name: "bad-app", endpoint: "https://bad.test/api/package" },
|
|
202
|
+
}]);
|
|
203
|
+
|
|
204
|
+
mockFetch.mockResolvedValueOnce(
|
|
205
|
+
new Response(
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
activities: [
|
|
208
|
+
{ name: "no_collection" },
|
|
209
|
+
{ name: "has_collection", collection: "main" },
|
|
210
|
+
],
|
|
211
|
+
}),
|
|
212
|
+
{ status: 200 },
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const result = await testEnv.run(resolveRemoteActivities, createPayload()) as Record<string, unknown>;
|
|
217
|
+
expect(Object.keys(result)).toHaveLength(1);
|
|
218
|
+
expect(result["app:bad-app:main:has_collection"]).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
});
|