@kapeta/local-cluster-service 0.60.2 → 0.61.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/CHANGELOG.md +19 -0
- package/dist/cjs/src/storm/event-parser.js +68 -5
- package/dist/cjs/src/storm/events.d.ts +20 -1
- package/dist/cjs/src/storm/routes.js +100 -45
- package/dist/cjs/src/storm/stormClient.d.ts +12 -0
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/cjs/test/storm/duplicate-entities-events.json +143 -0
- package/dist/cjs/test/storm/event-parser.test.js +31 -0
- package/dist/esm/src/storm/event-parser.js +68 -5
- package/dist/esm/src/storm/events.d.ts +20 -1
- package/dist/esm/src/storm/routes.js +100 -45
- package/dist/esm/src/storm/stormClient.d.ts +12 -0
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/dist/esm/test/storm/duplicate-entities-events.json +143 -0
- package/dist/esm/test/storm/event-parser.test.js +31 -0
- package/package.json +1 -1
- package/src/storm/event-parser.ts +101 -19
- package/src/storm/events.ts +25 -1
- package/src/storm/routes.ts +133 -57
- package/src/storm/stormClient.ts +19 -0
- package/test/storm/duplicate-entities-events.json +143 -0
- package/test/storm/event-parser.test.ts +39 -0
package/src/storm/routes.ts
CHANGED
@@ -13,7 +13,7 @@ import { stringBody } from '../middleware/stringBody';
|
|
13
13
|
import { KapetaBodyRequest } from '../types';
|
14
14
|
import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
|
15
15
|
import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt, UIPageEditRequest } from './stormClient';
|
16
|
-
import { Page, StormEvent, StormEventPage, StormEventPhaseType } from './events';
|
16
|
+
import { Page, StormEvent, StormEventPage, StormEventPhaseType, UIShell, UserJourneyScreen } from './events';
|
17
17
|
import {
|
18
18
|
createPhaseEndEvent,
|
19
19
|
createPhaseStartEvent,
|
@@ -132,7 +132,9 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
132
132
|
|
133
133
|
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
134
134
|
|
135
|
+
// Get user journeys
|
135
136
|
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
|
137
|
+
const outerConversationId = userJourneysStream.getConversationId();
|
136
138
|
|
137
139
|
onRequestAborted(req, res, () => {
|
138
140
|
userJourneysStream.abort();
|
@@ -140,23 +142,12 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
140
142
|
|
141
143
|
res.set('Content-Type', 'application/x-ndjson');
|
142
144
|
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
143
|
-
res.set(ConversationIdHeader,
|
145
|
+
res.set(ConversationIdHeader, outerConversationId);
|
144
146
|
|
145
|
-
const
|
147
|
+
const uniqueUserJourneyScreens: Record<string, UserJourneyScreen> = {};
|
146
148
|
|
147
|
-
|
148
|
-
onRequestAborted(req, res, () => {
|
149
|
-
queue.cancel();
|
150
|
-
});
|
151
|
-
|
152
|
-
const systemId = userJourneysStream.getConversationId();
|
153
|
-
|
154
|
-
UI_SERVERS[systemId] = new UIServer(systemId);
|
155
|
-
await UI_SERVERS[systemId].start();
|
156
|
-
|
157
|
-
userJourneysStream.on('data', async (data: StormEvent) => {
|
149
|
+
userJourneysStream.on('data', (data: StormEvent) => {
|
158
150
|
try {
|
159
|
-
console.log('Processing user journey event', data);
|
160
151
|
sendEvent(res, data);
|
161
152
|
if (data.type !== 'USER_JOURNEY') {
|
162
153
|
return;
|
@@ -167,54 +158,139 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
167
158
|
}
|
168
159
|
|
169
160
|
data.payload.screens.forEach((screen) => {
|
170
|
-
if (screen.name
|
171
|
-
|
161
|
+
if (!uniqueUserJourneyScreens[screen.name]) {
|
162
|
+
uniqueUserJourneyScreens[screen.name] = screen;
|
172
163
|
}
|
173
|
-
promises[screen.name] = queue.add(
|
174
|
-
() =>
|
175
|
-
new Promise(async (resolve, reject) => {
|
176
|
-
try {
|
177
|
-
const innerConversationId = uuid.v4();
|
178
|
-
const screenStream = await stormClient.createUIPage(
|
179
|
-
{
|
180
|
-
prompt: screen.requirements,
|
181
|
-
method: screen.method,
|
182
|
-
path: screen.path,
|
183
|
-
description: screen.requirements,
|
184
|
-
name: screen.name,
|
185
|
-
title: screen.title,
|
186
|
-
filename: screen.filename,
|
187
|
-
storage_prefix: userJourneysStream.getConversationId() + '_',
|
188
|
-
},
|
189
|
-
innerConversationId
|
190
|
-
);
|
191
|
-
const promises: Promise<void>[] = [];
|
192
|
-
screenStream.on('data', (screenData: StormEvent) => {
|
193
|
-
if (screenData.type === 'PAGE') {
|
194
|
-
screenData.payload.conversationId = innerConversationId;
|
195
|
-
promises.push(
|
196
|
-
sendPageEvent(userJourneysStream.getConversationId(), screenData, res)
|
197
|
-
);
|
198
|
-
} else {
|
199
|
-
sendEvent(res, screenData);
|
200
|
-
}
|
201
|
-
});
|
202
|
-
screenStream.on('end', () => {
|
203
|
-
Promise.allSettled(promises).finally(resolve);
|
204
|
-
});
|
205
|
-
} catch (e: any) {
|
206
|
-
console.error('Failed to process screen', e);
|
207
|
-
reject(e);
|
208
|
-
}
|
209
|
-
})
|
210
|
-
);
|
211
164
|
});
|
212
165
|
} catch (e) {
|
213
166
|
console.error('Failed to process event', e);
|
214
167
|
}
|
215
168
|
});
|
216
169
|
|
170
|
+
userJourneysStream.on('error', (error) => {
|
171
|
+
console.error('Error on userJourneysStream', error);
|
172
|
+
userJourneysStream.abort();
|
173
|
+
sendError(error, res);
|
174
|
+
});
|
175
|
+
|
217
176
|
await waitForStormStream(userJourneysStream);
|
177
|
+
|
178
|
+
// Get the UI shells
|
179
|
+
const shellsStream = await stormClient.createUIShells(
|
180
|
+
{
|
181
|
+
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
182
|
+
name: screen.name,
|
183
|
+
title: screen.title,
|
184
|
+
filename: screen.filename,
|
185
|
+
path: screen.path,
|
186
|
+
method: screen.method,
|
187
|
+
requirements: screen.requirements,
|
188
|
+
})),
|
189
|
+
},
|
190
|
+
conversationId
|
191
|
+
);
|
192
|
+
|
193
|
+
onRequestAborted(req, res, () => {
|
194
|
+
shellsStream.abort();
|
195
|
+
});
|
196
|
+
|
197
|
+
const uiShells: UIShell[] = [];
|
198
|
+
|
199
|
+
shellsStream.on('data', (data: StormEvent) => {
|
200
|
+
console.log('Processing shell event', data);
|
201
|
+
sendEvent(res, data);
|
202
|
+
|
203
|
+
if (data.type !== 'UI_SHELL') {
|
204
|
+
return;
|
205
|
+
}
|
206
|
+
|
207
|
+
if (shellsStream.isAborted()) {
|
208
|
+
return;
|
209
|
+
}
|
210
|
+
|
211
|
+
uiShells.push(data.payload);
|
212
|
+
});
|
213
|
+
|
214
|
+
shellsStream.on('error', (error) => {
|
215
|
+
console.error('Error on shellsStream', error);
|
216
|
+
shellsStream.abort();
|
217
|
+
sendError(error, res);
|
218
|
+
});
|
219
|
+
|
220
|
+
await waitForStormStream(shellsStream);
|
221
|
+
|
222
|
+
UI_SERVERS[outerConversationId] = new UIServer(outerConversationId);
|
223
|
+
await UI_SERVERS[outerConversationId].start();
|
224
|
+
|
225
|
+
// Get the pages (5 at a time)
|
226
|
+
const queue = new PromiseQueue(5);
|
227
|
+
|
228
|
+
onRequestAborted(req, res, () => {
|
229
|
+
queue.cancel();
|
230
|
+
});
|
231
|
+
|
232
|
+
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
233
|
+
await queue.add(
|
234
|
+
() =>
|
235
|
+
new Promise(async (resolve, reject) => {
|
236
|
+
try {
|
237
|
+
const innerConversationId = uuid.v4();
|
238
|
+
const screenStream = await stormClient.createUIPage(
|
239
|
+
{
|
240
|
+
prompt: screen.requirements,
|
241
|
+
method: screen.method,
|
242
|
+
path: screen.path,
|
243
|
+
description: screen.requirements,
|
244
|
+
name: screen.name,
|
245
|
+
title: screen.title,
|
246
|
+
filename: screen.filename,
|
247
|
+
storage_prefix: outerConversationId + '_',
|
248
|
+
shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
|
249
|
+
},
|
250
|
+
innerConversationId
|
251
|
+
);
|
252
|
+
|
253
|
+
const promiseList: Promise<void>[] = [];
|
254
|
+
screenStream.on('data', (screenData: StormEvent) => {
|
255
|
+
if (screenData.type === 'PAGE') {
|
256
|
+
promiseList.push(
|
257
|
+
sendPageEvent(
|
258
|
+
outerConversationId,
|
259
|
+
{
|
260
|
+
...screenData,
|
261
|
+
payload: {
|
262
|
+
...screenData.payload,
|
263
|
+
conversationId: innerConversationId,
|
264
|
+
},
|
265
|
+
},
|
266
|
+
res
|
267
|
+
)
|
268
|
+
);
|
269
|
+
} else {
|
270
|
+
sendEvent(res, screenData);
|
271
|
+
}
|
272
|
+
});
|
273
|
+
|
274
|
+
screenStream.on('end', async () => {
|
275
|
+
try {
|
276
|
+
await Promise.allSettled(promiseList).finally(() => resolve(true));
|
277
|
+
} catch (error) {
|
278
|
+
console.error('Failed to process screen', error);
|
279
|
+
}
|
280
|
+
});
|
281
|
+
|
282
|
+
screenStream.on('error', (error) => {
|
283
|
+
console.error('Error on screenStream', error);
|
284
|
+
screenStream.abort();
|
285
|
+
});
|
286
|
+
} catch (e) {
|
287
|
+
console.error('Failed to process screen', e);
|
288
|
+
reject(e);
|
289
|
+
}
|
290
|
+
})
|
291
|
+
);
|
292
|
+
}
|
293
|
+
|
218
294
|
await queue.wait();
|
219
295
|
|
220
296
|
if (userJourneysStream.isAborted()) {
|
@@ -222,8 +298,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
222
298
|
}
|
223
299
|
|
224
300
|
sendDone(res);
|
225
|
-
} catch (err
|
226
|
-
sendError(err, res);
|
301
|
+
} catch (err) {
|
302
|
+
sendError(err as Error, res);
|
227
303
|
if (!res.closed) {
|
228
304
|
res.end();
|
229
305
|
}
|
package/src/storm/stormClient.ts
CHANGED
@@ -21,6 +21,17 @@ export const STORM_ID = 'storm';
|
|
21
21
|
|
22
22
|
export const ConversationIdHeader = 'Conversation-Id';
|
23
23
|
|
24
|
+
export interface UIShellsPrompt {
|
25
|
+
pages: {
|
26
|
+
name: string;
|
27
|
+
title: string;
|
28
|
+
filename: string;
|
29
|
+
path: string;
|
30
|
+
method: string;
|
31
|
+
requirements: string;
|
32
|
+
}[];
|
33
|
+
}
|
34
|
+
|
24
35
|
export interface UIPagePrompt {
|
25
36
|
name: string;
|
26
37
|
title: string;
|
@@ -30,6 +41,7 @@ export interface UIPagePrompt {
|
|
30
41
|
method: string;
|
31
42
|
description: string;
|
32
43
|
storage_prefix: string;
|
44
|
+
shell_page?: string;
|
33
45
|
}
|
34
46
|
|
35
47
|
export interface UIPageEditPrompt {
|
@@ -153,6 +165,13 @@ class StormClient {
|
|
153
165
|
});
|
154
166
|
}
|
155
167
|
|
168
|
+
public createUIShells(prompt: UIShellsPrompt, conversationId?: string) {
|
169
|
+
return this.send('/v2/ui/shells', {
|
170
|
+
prompt: JSON.stringify(prompt),
|
171
|
+
conversationId,
|
172
|
+
});
|
173
|
+
}
|
174
|
+
|
156
175
|
public createUIPage(prompt: UIPagePrompt, conversationId?: string) {
|
157
176
|
return this.send('/v2/ui/page', {
|
158
177
|
prompt: prompt,
|
@@ -0,0 +1,143 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"type": "CREATE_BLOCK",
|
4
|
+
"reason": "Block updated",
|
5
|
+
"payload": {
|
6
|
+
"archetype": "",
|
7
|
+
"description": "Handles traffic from the backend services.",
|
8
|
+
"name": "api-gateway",
|
9
|
+
"resources": [
|
10
|
+
{
|
11
|
+
"description": "",
|
12
|
+
"name": "posts",
|
13
|
+
"to": "post-service",
|
14
|
+
"type": "CLIENT"
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"description": "",
|
18
|
+
"name": "comments",
|
19
|
+
"to": "comment-service",
|
20
|
+
"type": "CLIENT"
|
21
|
+
}
|
22
|
+
],
|
23
|
+
"type": "GATEWAY",
|
24
|
+
"blockRef": "kapeta://kapeta/api-gateway:local",
|
25
|
+
"instanceId": "6b247f30-dec4-5960-a8a0-f300caa95226"
|
26
|
+
},
|
27
|
+
"created": 1720784242064
|
28
|
+
},
|
29
|
+
{
|
30
|
+
"type": "CREATE_BLOCK",
|
31
|
+
"reason": "Block updated",
|
32
|
+
"payload": {
|
33
|
+
"archetype": "",
|
34
|
+
"description": "Manages the creation, editing, and deletion of blog posts.",
|
35
|
+
"name": "post-service",
|
36
|
+
"resources": [
|
37
|
+
{
|
38
|
+
"description": "The posts API provides endpoints for managing blog posts. It includes functionality for creating, editing, and deleting posts, as well as retrieving and searching for posts. The API is designed to be secure and follows best practices for authentication and authorization.",
|
39
|
+
"name": "posts",
|
40
|
+
"type": "API"
|
41
|
+
}
|
42
|
+
],
|
43
|
+
"type": "BACKEND",
|
44
|
+
"blockRef": "kapeta://kapeta/post-service:local",
|
45
|
+
"instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
|
46
|
+
},
|
47
|
+
"created": 1720784243137
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"type": "CREATE_BLOCK",
|
51
|
+
"reason": "Block updated",
|
52
|
+
"payload": {
|
53
|
+
"archetype": "",
|
54
|
+
"description": "Manages the creation, editing, and deletion of comments on blog posts.",
|
55
|
+
"name": "comment-service",
|
56
|
+
"resources": [
|
57
|
+
{
|
58
|
+
"description": "The comments API provides endpoints for managing comments on blog posts. It includes functionality for creating, editing, and deleting comments, as well as retrieving and searching for comments. The API is designed to be secure and follows best practices for authentication and authorization.",
|
59
|
+
"name": "comments",
|
60
|
+
"type": "API"
|
61
|
+
}
|
62
|
+
],
|
63
|
+
"type": "BACKEND",
|
64
|
+
"blockRef": "kapeta://kapeta/comment-service:local",
|
65
|
+
"instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
|
66
|
+
},
|
67
|
+
"created": 1720784243141
|
68
|
+
},
|
69
|
+
{
|
70
|
+
"type": "CREATE_TYPE",
|
71
|
+
"reason": "Create type for post-service",
|
72
|
+
"payload": {
|
73
|
+
"blockName": "post-service",
|
74
|
+
"content": "enum Status {\n NEW,\n ARCHIVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
|
75
|
+
"blockRef": "kapeta://kapeta/post-service:local",
|
76
|
+
"instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
|
77
|
+
},
|
78
|
+
"created": 1720784233286
|
79
|
+
},
|
80
|
+
{
|
81
|
+
"type": "CREATE_API",
|
82
|
+
"reason": "Create API for post-service",
|
83
|
+
"payload": {
|
84
|
+
"blockName": "post-service",
|
85
|
+
"content": "@GET(\"/status\")\ngetStatus(): Result",
|
86
|
+
"blockRef": "kapeta://kapeta/post-service:local",
|
87
|
+
"instanceId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26"
|
88
|
+
},
|
89
|
+
"created": 1720784233290
|
90
|
+
},
|
91
|
+
{
|
92
|
+
"type": "CREATE_TYPE",
|
93
|
+
"reason": "Create type for comment-service",
|
94
|
+
"payload": {
|
95
|
+
"blockName": "comment-service",
|
96
|
+
"content": "enum Status {\n NEW,\n APPROVED\n}\n\ntype Result {\n status: Status\n}\n\ntype User {\n id: string\n}",
|
97
|
+
"blockRef": "kapeta://kapeta/comment-service:local",
|
98
|
+
"instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
|
99
|
+
},
|
100
|
+
"created": 1720784241000
|
101
|
+
},
|
102
|
+
{
|
103
|
+
"type": "CREATE_API",
|
104
|
+
"reason": "Create API for comment-service",
|
105
|
+
"payload": {
|
106
|
+
"blockName": "comment-service",
|
107
|
+
"content": "@GET(\"/status\")\ngetStatus(): Result",
|
108
|
+
"blockRef": "kapeta://kapeta/comment-service:local",
|
109
|
+
"instanceId": "d4215220-f552-5a6d-9429-bef1c40c4d7c"
|
110
|
+
},
|
111
|
+
"created": 1720784241002
|
112
|
+
},
|
113
|
+
{
|
114
|
+
"type": "CREATE_CONNECTION",
|
115
|
+
"reason": "api-gateway needs to be able to serve the posts API from the post-service",
|
116
|
+
"payload": {
|
117
|
+
"fromComponent": "post-service",
|
118
|
+
"fromResource": "posts",
|
119
|
+
"fromResourceType": "API",
|
120
|
+
"toComponent": "api-gateway",
|
121
|
+
"toResource": "posts",
|
122
|
+
"toResourceType": "CLIENT",
|
123
|
+
"fromBlockId": "4e002962-ca24-5b8b-bcea-395b9bbf7c26",
|
124
|
+
"toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
|
125
|
+
},
|
126
|
+
"created": 1720784243173
|
127
|
+
},
|
128
|
+
{
|
129
|
+
"type": "CREATE_CONNECTION",
|
130
|
+
"reason": "api-gateway needs to be able to serve the comments API from the comment-service",
|
131
|
+
"payload": {
|
132
|
+
"fromComponent": "comment-service",
|
133
|
+
"fromResource": "comments",
|
134
|
+
"fromResourceType": "API",
|
135
|
+
"toComponent": "api-gateway",
|
136
|
+
"toResource": "comments",
|
137
|
+
"toResourceType": "CLIENT",
|
138
|
+
"fromBlockId": "d4215220-f552-5a6d-9429-bef1c40c4d7c",
|
139
|
+
"toBlockId": "6b247f30-dec4-5960-a8a0-f300caa95226"
|
140
|
+
},
|
141
|
+
"created": 1720784243174
|
142
|
+
}
|
143
|
+
]
|
@@ -8,6 +8,8 @@ import { StormEvent } from '../../src/storm/events';
|
|
8
8
|
import simpleBlogEvents from './simple-blog-events.json';
|
9
9
|
import predefinedUserEvents from './predefined-user-events.json';
|
10
10
|
import testEvents from './blog-events.json';
|
11
|
+
import duplicateEntitiesEvents from './duplicate-entities-events.json';
|
12
|
+
import { DSLAPIParser, DSLDataType, DSLDataTypeParser, DSLMethod } from '@kapeta/kaplang-core';
|
11
13
|
|
12
14
|
export const parserOptions = {
|
13
15
|
serviceKind: 'kapeta/block-service:local',
|
@@ -245,4 +247,41 @@ describe('event-parser', () => {
|
|
245
247
|
const safeName = StormEventParser.toSafeArtifactName('Browser-based CRM Application');
|
246
248
|
expect(safeName).toBe('browserbasedcrmapplication');
|
247
249
|
});
|
250
|
+
|
251
|
+
it('rename duplicate entity names', async () => {
|
252
|
+
const events = duplicateEntitiesEvents as StormEvent[];
|
253
|
+
const parser = new StormEventParser(parserOptions);
|
254
|
+
for (const event of events) {
|
255
|
+
await parser.processEvent('kapeta', event);
|
256
|
+
}
|
257
|
+
|
258
|
+
const result = await parser.toResult('kapeta');
|
259
|
+
const apiGateway = result.blocks.find((block) => block.aiName === 'api-gateway');
|
260
|
+
expect(apiGateway).toBeDefined();
|
261
|
+
|
262
|
+
const dataTypes = DSLDataTypeParser.parse(apiGateway!.content!.spec!.entities!.source!.value, {
|
263
|
+
ignoreSemantics: true,
|
264
|
+
});
|
265
|
+
expect(dataTypes.map((type) => type.name)).toStrictEqual(['Status', 'Result', 'User', 'Status_1', 'Result_1']);
|
266
|
+
|
267
|
+
const conflictingType = dataTypes.find((type) => type.name === 'Result_1');
|
268
|
+
expect(conflictingType).toBeDefined();
|
269
|
+
const dataType = conflictingType as DSLDataType;
|
270
|
+
expect(dataType.properties?.length).toBe(1);
|
271
|
+
const dslDataTypeProperty = dataType.properties![0];
|
272
|
+
expect(dslDataTypeProperty.type).toBe('Status_1');
|
273
|
+
|
274
|
+
const commentsClient = apiGateway?.content!.spec!.consumers?.find(
|
275
|
+
(resource) => resource.metadata.name === 'comments'
|
276
|
+
);
|
277
|
+
expect(commentsClient).toBeDefined();
|
278
|
+
|
279
|
+
const methods = DSLAPIParser.parse(commentsClient!.spec!.source!.value, {
|
280
|
+
ignoreSemantics: true,
|
281
|
+
});
|
282
|
+
expect(methods).toBeDefined();
|
283
|
+
expect(methods.length).toBe(1);
|
284
|
+
const method = methods[0] as DSLMethod;
|
285
|
+
expect(method.returnType).toBe('Result_1');
|
286
|
+
});
|
248
287
|
});
|