@kapeta/local-cluster-service 0.58.6 → 0.59.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.59.0](https://github.com/kapetacom/local-cluster-service/compare/v0.58.6...v0.59.0) (2024-08-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * Convert PAGE events to PAGE_URL ([#207](https://github.com/kapetacom/local-cluster-service/issues/207)) ([9fddeec](https://github.com/kapetacom/local-cluster-service/commit/9fddeec4a055a02796d80987ae5b9ca39d7dd9ee))
7
+
1
8
  ## [0.58.6](https://github.com/kapetacom/local-cluster-service/compare/v0.58.5...v0.58.6) (2024-08-01)
2
9
 
3
10
 
@@ -272,12 +272,14 @@ export interface StormEventPhases {
272
272
  }
273
273
  export interface Page {
274
274
  name: string;
275
+ filename: string;
275
276
  title: string;
276
277
  description: string;
277
278
  content: string;
278
279
  path: string;
279
280
  method: string;
280
281
  conversationId: string;
282
+ prompt: string;
281
283
  }
282
284
  export interface StormEventPage {
283
285
  type: 'PAGE';
@@ -285,6 +287,23 @@ export interface StormEventPage {
285
287
  created: number;
286
288
  payload: Page;
287
289
  }
290
+ export interface StormEventPageUrl {
291
+ type: 'PAGE_URL';
292
+ reason: string;
293
+ created: number;
294
+ payload: {
295
+ id: string;
296
+ name: string;
297
+ filename: string;
298
+ title: string;
299
+ description: string;
300
+ path: string;
301
+ url: string;
302
+ method: string;
303
+ conversationId: string;
304
+ prompt: string;
305
+ };
306
+ }
288
307
  export interface UserJourneyScreen {
289
308
  name: string;
290
309
  title: string;
@@ -304,5 +323,5 @@ export interface StormEventUserJourney {
304
323
  created: number;
305
324
  payload: UserJourney;
306
325
  }
307
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
326
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
308
327
  export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ export declare const SystemIdHeader = "System-Id";
8
+ export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
9
+ path: string;
10
+ }>;
11
+ export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
12
+ export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ exports.SystemIdHeader = 'System-Id';
11
+ async function writePageToDisk(systemId, event) {
12
+ const path = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, event.payload.path, event.payload.method.toLowerCase(), 'index.html');
13
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
14
+ await fs_extra_1.default.writeFile(path, event.payload.content);
15
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
16
+ return {
17
+ path,
18
+ };
19
+ }
20
+ exports.writePageToDisk = writePageToDisk;
21
+ function readPageFromDiskAsString(systemId, path, method) {
22
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
23
+ if (!fs_extra_1.default.existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ return fs_extra_1.default.readFileSync(filePath, 'utf8');
27
+ }
28
+ exports.readPageFromDiskAsString = readPageFromDiskAsString;
29
+ function readPageFromDisk(systemId, path, method, res) {
30
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
31
+ if (!fs_extra_1.default.existsSync(filePath)) {
32
+ res.status(404).send('Page not found');
33
+ return;
34
+ }
35
+ res.type(filePath.split('.').pop());
36
+ const content = fs_extra_1.default.readFileSync(filePath, 'utf8');
37
+ res.write(content);
38
+ res.end();
39
+ }
40
+ exports.readPageFromDisk = readPageFromDisk;
@@ -20,14 +20,43 @@ const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
+ const page_utils_1 = require("./page-utils");
23
24
  const router = (0, express_promise_router_1.default)();
24
25
  router.use('/', cors_1.corsHandler);
25
26
  router.use('/', stringBody_1.stringBody);
26
- router.post('/:handle/ui/screen', async (req, res) => {
27
- const handle = req.params.handle;
27
+ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
28
+ if (screenData.type === 'PAGE') {
29
+ screenData.payload.conversationId = innerConversationId;
30
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
31
+ return {
32
+ type: 'PAGE_URL',
33
+ reason: screenData.reason,
34
+ created: screenData.created,
35
+ payload: {
36
+ id: node_uuid_1.default.v4(),
37
+ name: screenData.payload.name,
38
+ title: screenData.payload.title,
39
+ filename: screenData.payload.filename,
40
+ description: screenData.payload.description,
41
+ prompt: screenData.payload.prompt,
42
+ path: screenData.payload.path,
43
+ url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
44
+ method: screenData.payload.method,
45
+ conversationId: innerConversationId,
46
+ },
47
+ };
48
+ }
49
+ return screenData;
50
+ }
51
+ router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
52
+ (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
53
+ });
54
+ router.post('/ui/screen', async (req, res) => {
28
55
  try {
29
56
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
57
+ const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
30
58
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
59
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
31
60
  const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
32
61
  onRequestAborted(req, res, () => {
33
62
  screenStream.abort();
@@ -35,11 +64,21 @@ router.post('/:handle/ui/screen', async (req, res) => {
35
64
  res.set('Content-Type', 'application/x-ndjson');
36
65
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
37
66
  res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
67
+ const promises = [];
38
68
  screenStream.on('data', (data) => {
39
- console.log('Processing screen event', data);
69
+ switch (data.type) {
70
+ case 'PAGE':
71
+ console.log('Processing page event', data);
72
+ data.payload.conversationId = screenStream.getConversationId();
73
+ if (systemId) {
74
+ promises.push(sendPageEvent(systemId, data, res));
75
+ }
76
+ break;
77
+ }
40
78
  sendEvent(res, data);
41
79
  });
42
80
  await waitForStormStream(screenStream);
81
+ await Promise.allSettled(promises);
43
82
  sendDone(res);
44
83
  }
45
84
  catch (err) {
@@ -62,7 +101,7 @@ router.post('/:handle/ui', async (req, res) => {
62
101
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
63
102
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
64
103
  const promises = {};
65
- const queue = new PromiseQueue_1.PromiseQueue(10);
104
+ const queue = new PromiseQueue_1.PromiseQueue(5);
66
105
  onRequestAborted(req, res, () => {
67
106
  queue.cancel();
68
107
  });
@@ -91,15 +130,20 @@ router.post('/:handle/ui', async (req, res) => {
91
130
  name: screen.name,
92
131
  title: screen.title,
93
132
  filename: screen.filename,
133
+ storage_prefix: userJourneysStream.getConversationId() + '_',
94
134
  }, innerConversationId);
135
+ const promises = [];
95
136
  screenStream.on('data', (screenData) => {
96
137
  if (screenData.type === 'PAGE') {
97
138
  screenData.payload.conversationId = innerConversationId;
139
+ promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
140
+ }
141
+ else {
142
+ sendEvent(res, screenData);
98
143
  }
99
- sendEvent(res, screenData);
100
144
  });
101
145
  screenStream.on('end', () => {
102
- resolve();
146
+ Promise.allSettled(promises).finally(resolve);
103
147
  });
104
148
  }
105
149
  catch (e) {
@@ -131,16 +175,45 @@ router.post('/ui/edit', async (req, res) => {
131
175
  try {
132
176
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
133
177
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
134
- const editStream = await stormClient_1.stormClient.editPages(aiRequest.prompt, conversationId);
178
+ const pages = aiRequest.prompt.pages
179
+ .map((page) => {
180
+ const content = (0, page_utils_1.readPageFromDiskAsString)(conversationId, page.path, page.method);
181
+ if (!content) {
182
+ console.warn('Page not found', page);
183
+ return undefined;
184
+ }
185
+ return {
186
+ filename: page.filename,
187
+ path: page.path,
188
+ method: page.method,
189
+ title: page.title,
190
+ conversationId: page.conversationId,
191
+ prompt: page.prompt,
192
+ name: page.name,
193
+ description: page.description,
194
+ content,
195
+ };
196
+ })
197
+ .filter((page) => !!page);
198
+ const editStream = await stormClient_1.stormClient.editPages({
199
+ ...aiRequest.prompt,
200
+ pages,
201
+ }, conversationId);
135
202
  onRequestAborted(req, res, () => {
136
203
  editStream.abort();
137
204
  });
138
205
  res.set('Content-Type', 'application/x-ndjson');
139
206
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
140
207
  res.set(stormClient_1.ConversationIdHeader, editStream.getConversationId());
208
+ const promises = [];
141
209
  editStream.on('data', (data) => {
142
210
  try {
143
- sendEvent(res, data);
211
+ if (data.type === 'PAGE') {
212
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
213
+ }
214
+ else {
215
+ sendEvent(res, data);
216
+ }
144
217
  }
145
218
  catch (e) {
146
219
  console.error('Failed to process event', e);
@@ -150,6 +223,7 @@ router.post('/ui/edit', async (req, res) => {
150
223
  if (editStream.isAborted()) {
151
224
  return;
152
225
  }
226
+ await Promise.all(promises);
153
227
  sendDone(res);
154
228
  }
155
229
  catch (err) {
@@ -351,4 +425,13 @@ function onRequestAborted(req, res, onAborted) {
351
425
  onAborted();
352
426
  });
353
427
  }
428
+ function sendPageEvent(mainConversationId, data, res) {
429
+ return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
430
+ .catch((err) => {
431
+ console.error('Failed to write page to disk', err);
432
+ })
433
+ .then(() => {
434
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
435
+ });
436
+ }
354
437
  exports.default = router;
@@ -1,4 +1,5 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { Page, StormEventPageUrl } from './events';
2
3
  export declare const STORM_ID = "storm";
3
4
  export declare const ConversationIdHeader = "Conversation-Id";
4
5
  export interface UIPagePrompt {
@@ -9,14 +10,18 @@ export interface UIPagePrompt {
9
10
  path: string;
10
11
  method: string;
11
12
  description: string;
13
+ storage_prefix: string;
12
14
  }
13
15
  export interface UIPageEditPrompt {
14
16
  planDescription: string;
15
17
  blockDescription: string;
16
- pages: {
17
- filename: string;
18
- content: string;
19
- }[];
18
+ pages: Page[];
19
+ prompt: string;
20
+ }
21
+ export interface UIPageEditRequest {
22
+ planDescription: string;
23
+ blockDescription: string;
24
+ pages: StormEventPageUrl['payload'][];
20
25
  prompt: string;
21
26
  }
22
27
  declare class StormClient {
@@ -272,12 +272,14 @@ export interface StormEventPhases {
272
272
  }
273
273
  export interface Page {
274
274
  name: string;
275
+ filename: string;
275
276
  title: string;
276
277
  description: string;
277
278
  content: string;
278
279
  path: string;
279
280
  method: string;
280
281
  conversationId: string;
282
+ prompt: string;
281
283
  }
282
284
  export interface StormEventPage {
283
285
  type: 'PAGE';
@@ -285,6 +287,23 @@ export interface StormEventPage {
285
287
  created: number;
286
288
  payload: Page;
287
289
  }
290
+ export interface StormEventPageUrl {
291
+ type: 'PAGE_URL';
292
+ reason: string;
293
+ created: number;
294
+ payload: {
295
+ id: string;
296
+ name: string;
297
+ filename: string;
298
+ title: string;
299
+ description: string;
300
+ path: string;
301
+ url: string;
302
+ method: string;
303
+ conversationId: string;
304
+ prompt: string;
305
+ };
306
+ }
288
307
  export interface UserJourneyScreen {
289
308
  name: string;
290
309
  title: string;
@@ -304,5 +323,5 @@ export interface StormEventUserJourney {
304
323
  created: number;
305
324
  payload: UserJourney;
306
325
  }
307
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
326
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
308
327
  export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ export declare const SystemIdHeader = "System-Id";
8
+ export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
9
+ path: string;
10
+ }>;
11
+ export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
12
+ export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ exports.SystemIdHeader = 'System-Id';
11
+ async function writePageToDisk(systemId, event) {
12
+ const path = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, event.payload.path, event.payload.method.toLowerCase(), 'index.html');
13
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
14
+ await fs_extra_1.default.writeFile(path, event.payload.content);
15
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
16
+ return {
17
+ path,
18
+ };
19
+ }
20
+ exports.writePageToDisk = writePageToDisk;
21
+ function readPageFromDiskAsString(systemId, path, method) {
22
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
23
+ if (!fs_extra_1.default.existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ return fs_extra_1.default.readFileSync(filePath, 'utf8');
27
+ }
28
+ exports.readPageFromDiskAsString = readPageFromDiskAsString;
29
+ function readPageFromDisk(systemId, path, method, res) {
30
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
31
+ if (!fs_extra_1.default.existsSync(filePath)) {
32
+ res.status(404).send('Page not found');
33
+ return;
34
+ }
35
+ res.type(filePath.split('.').pop());
36
+ const content = fs_extra_1.default.readFileSync(filePath, 'utf8');
37
+ res.write(content);
38
+ res.end();
39
+ }
40
+ exports.readPageFromDisk = readPageFromDisk;
@@ -20,14 +20,43 @@ const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
+ const page_utils_1 = require("./page-utils");
23
24
  const router = (0, express_promise_router_1.default)();
24
25
  router.use('/', cors_1.corsHandler);
25
26
  router.use('/', stringBody_1.stringBody);
26
- router.post('/:handle/ui/screen', async (req, res) => {
27
- const handle = req.params.handle;
27
+ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
28
+ if (screenData.type === 'PAGE') {
29
+ screenData.payload.conversationId = innerConversationId;
30
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
31
+ return {
32
+ type: 'PAGE_URL',
33
+ reason: screenData.reason,
34
+ created: screenData.created,
35
+ payload: {
36
+ id: node_uuid_1.default.v4(),
37
+ name: screenData.payload.name,
38
+ title: screenData.payload.title,
39
+ filename: screenData.payload.filename,
40
+ description: screenData.payload.description,
41
+ prompt: screenData.payload.prompt,
42
+ path: screenData.payload.path,
43
+ url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
44
+ method: screenData.payload.method,
45
+ conversationId: innerConversationId,
46
+ },
47
+ };
48
+ }
49
+ return screenData;
50
+ }
51
+ router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
52
+ (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
53
+ });
54
+ router.post('/ui/screen', async (req, res) => {
28
55
  try {
29
56
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
57
+ const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
30
58
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
59
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
31
60
  const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
32
61
  onRequestAborted(req, res, () => {
33
62
  screenStream.abort();
@@ -35,11 +64,21 @@ router.post('/:handle/ui/screen', async (req, res) => {
35
64
  res.set('Content-Type', 'application/x-ndjson');
36
65
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
37
66
  res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
67
+ const promises = [];
38
68
  screenStream.on('data', (data) => {
39
- console.log('Processing screen event', data);
69
+ switch (data.type) {
70
+ case 'PAGE':
71
+ console.log('Processing page event', data);
72
+ data.payload.conversationId = screenStream.getConversationId();
73
+ if (systemId) {
74
+ promises.push(sendPageEvent(systemId, data, res));
75
+ }
76
+ break;
77
+ }
40
78
  sendEvent(res, data);
41
79
  });
42
80
  await waitForStormStream(screenStream);
81
+ await Promise.allSettled(promises);
43
82
  sendDone(res);
44
83
  }
45
84
  catch (err) {
@@ -62,7 +101,7 @@ router.post('/:handle/ui', async (req, res) => {
62
101
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
63
102
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
64
103
  const promises = {};
65
- const queue = new PromiseQueue_1.PromiseQueue(10);
104
+ const queue = new PromiseQueue_1.PromiseQueue(5);
66
105
  onRequestAborted(req, res, () => {
67
106
  queue.cancel();
68
107
  });
@@ -91,15 +130,20 @@ router.post('/:handle/ui', async (req, res) => {
91
130
  name: screen.name,
92
131
  title: screen.title,
93
132
  filename: screen.filename,
133
+ storage_prefix: userJourneysStream.getConversationId() + '_',
94
134
  }, innerConversationId);
135
+ const promises = [];
95
136
  screenStream.on('data', (screenData) => {
96
137
  if (screenData.type === 'PAGE') {
97
138
  screenData.payload.conversationId = innerConversationId;
139
+ promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
140
+ }
141
+ else {
142
+ sendEvent(res, screenData);
98
143
  }
99
- sendEvent(res, screenData);
100
144
  });
101
145
  screenStream.on('end', () => {
102
- resolve();
146
+ Promise.allSettled(promises).finally(resolve);
103
147
  });
104
148
  }
105
149
  catch (e) {
@@ -131,16 +175,45 @@ router.post('/ui/edit', async (req, res) => {
131
175
  try {
132
176
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
133
177
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
134
- const editStream = await stormClient_1.stormClient.editPages(aiRequest.prompt, conversationId);
178
+ const pages = aiRequest.prompt.pages
179
+ .map((page) => {
180
+ const content = (0, page_utils_1.readPageFromDiskAsString)(conversationId, page.path, page.method);
181
+ if (!content) {
182
+ console.warn('Page not found', page);
183
+ return undefined;
184
+ }
185
+ return {
186
+ filename: page.filename,
187
+ path: page.path,
188
+ method: page.method,
189
+ title: page.title,
190
+ conversationId: page.conversationId,
191
+ prompt: page.prompt,
192
+ name: page.name,
193
+ description: page.description,
194
+ content,
195
+ };
196
+ })
197
+ .filter((page) => !!page);
198
+ const editStream = await stormClient_1.stormClient.editPages({
199
+ ...aiRequest.prompt,
200
+ pages,
201
+ }, conversationId);
135
202
  onRequestAborted(req, res, () => {
136
203
  editStream.abort();
137
204
  });
138
205
  res.set('Content-Type', 'application/x-ndjson');
139
206
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
140
207
  res.set(stormClient_1.ConversationIdHeader, editStream.getConversationId());
208
+ const promises = [];
141
209
  editStream.on('data', (data) => {
142
210
  try {
143
- sendEvent(res, data);
211
+ if (data.type === 'PAGE') {
212
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
213
+ }
214
+ else {
215
+ sendEvent(res, data);
216
+ }
144
217
  }
145
218
  catch (e) {
146
219
  console.error('Failed to process event', e);
@@ -150,6 +223,7 @@ router.post('/ui/edit', async (req, res) => {
150
223
  if (editStream.isAborted()) {
151
224
  return;
152
225
  }
226
+ await Promise.all(promises);
153
227
  sendDone(res);
154
228
  }
155
229
  catch (err) {
@@ -351,4 +425,13 @@ function onRequestAborted(req, res, onAborted) {
351
425
  onAborted();
352
426
  });
353
427
  }
428
+ function sendPageEvent(mainConversationId, data, res) {
429
+ return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
430
+ .catch((err) => {
431
+ console.error('Failed to write page to disk', err);
432
+ })
433
+ .then(() => {
434
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
435
+ });
436
+ }
354
437
  exports.default = router;
@@ -1,4 +1,5 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { Page, StormEventPageUrl } from './events';
2
3
  export declare const STORM_ID = "storm";
3
4
  export declare const ConversationIdHeader = "Conversation-Id";
4
5
  export interface UIPagePrompt {
@@ -9,14 +10,18 @@ export interface UIPagePrompt {
9
10
  path: string;
10
11
  method: string;
11
12
  description: string;
13
+ storage_prefix: string;
12
14
  }
13
15
  export interface UIPageEditPrompt {
14
16
  planDescription: string;
15
17
  blockDescription: string;
16
- pages: {
17
- filename: string;
18
- content: string;
19
- }[];
18
+ pages: Page[];
19
+ prompt: string;
20
+ }
21
+ export interface UIPageEditRequest {
22
+ planDescription: string;
23
+ blockDescription: string;
24
+ pages: StormEventPageUrl['payload'][];
20
25
  prompt: string;
21
26
  }
22
27
  declare class StormClient {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.58.6",
3
+ "version": "0.59.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -324,12 +324,14 @@ export interface StormEventPhases {
324
324
 
325
325
  export interface Page {
326
326
  name: string;
327
+ filename: string;
327
328
  title: string;
328
329
  description: string;
329
330
  content: string;
330
331
  path: string;
331
332
  method: string;
332
333
  conversationId: string;
334
+ prompt: string;
333
335
  }
334
336
 
335
337
  // Event for creating a page
@@ -340,6 +342,25 @@ export interface StormEventPage {
340
342
  payload: Page;
341
343
  }
342
344
 
345
+ // Event for creating a page
346
+ export interface StormEventPageUrl {
347
+ type: 'PAGE_URL';
348
+ reason: string;
349
+ created: number;
350
+ payload: {
351
+ id: string;
352
+ name: string;
353
+ filename: string;
354
+ title: string;
355
+ description: string;
356
+ path: string;
357
+ url: string;
358
+ method: string;
359
+ conversationId: string;
360
+ prompt: string;
361
+ };
362
+ }
363
+
343
364
  export interface UserJourneyScreen {
344
365
  name: string;
345
366
  title: string;
@@ -389,4 +410,5 @@ export type StormEvent =
389
410
  | StormEventBlockStatus
390
411
  | StormEventCreateDSLRetry
391
412
  | StormEventUserJourney
392
- | StormEventPage;
413
+ | StormEventPage
414
+ | StormEventPageUrl;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ import os from 'node:os';
8
+ import Path from 'path';
9
+ import FS from 'fs-extra';
10
+
11
+ export const SystemIdHeader = 'System-Id';
12
+
13
+ export async function writePageToDisk(systemId: string, event: StormEventPage) {
14
+ const path = Path.join(
15
+ os.tmpdir(),
16
+ 'ai-systems',
17
+ systemId,
18
+ event.payload.path,
19
+ event.payload.method.toLowerCase(),
20
+ 'index.html'
21
+ );
22
+ await FS.ensureDir(Path.dirname(path));
23
+ await FS.writeFile(path, event.payload.content);
24
+
25
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
26
+
27
+ return {
28
+ path,
29
+ };
30
+ }
31
+
32
+ export function readPageFromDiskAsString(systemId: string, path: string, method: string) {
33
+ const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
34
+ if (!FS.existsSync(filePath)) {
35
+ return null;
36
+ }
37
+
38
+ return FS.readFileSync(filePath, 'utf8');
39
+ }
40
+
41
+ export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
42
+ const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
43
+ if (!FS.existsSync(filePath)) {
44
+ res.status(404).send('Page not found');
45
+ return;
46
+ }
47
+
48
+ res.type(filePath.split('.').pop() as string);
49
+
50
+ const content = FS.readFileSync(filePath, 'utf8');
51
+ res.write(content);
52
+ res.end();
53
+ }
@@ -12,8 +12,8 @@ import { corsHandler } from '../middleware/cors';
12
12
  import { stringBody } from '../middleware/stringBody';
13
13
  import { KapetaBodyRequest } from '../types';
14
14
  import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
15
- import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt } from './stormClient';
16
- import { StormEvent, StormEventPhaseType } from './events';
15
+ import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt, UIPageEditRequest } from './stormClient';
16
+ import { Page, StormEvent, StormEventPage, StormEventPhaseType } from './events';
17
17
  import {
18
18
  createPhaseEndEvent,
19
19
  createPhaseStartEvent,
@@ -25,18 +25,50 @@ import { StormCodegen } from './codegen';
25
25
  import { assetManager } from '../assetManager';
26
26
  import uuid from 'node-uuid';
27
27
  import { PromiseQueue } from './PromiseQueue';
28
+ import { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
28
29
 
29
30
  const router = Router();
30
31
 
31
32
  router.use('/', corsHandler);
32
33
  router.use('/', stringBody);
33
34
 
34
- router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
35
- const handle = req.params.handle as string;
35
+ function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
36
+ if (screenData.type === 'PAGE') {
37
+ screenData.payload.conversationId = innerConversationId;
38
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
39
+ return {
40
+ type: 'PAGE_URL',
41
+ reason: screenData.reason,
42
+ created: screenData.created,
43
+ payload: {
44
+ id: uuid.v4(),
45
+ name: screenData.payload.name,
46
+ title: screenData.payload.title,
47
+ filename: screenData.payload.filename,
48
+ description: screenData.payload.description,
49
+ prompt: screenData.payload.prompt,
50
+ path: screenData.payload.path,
51
+ url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
52
+ method: screenData.payload.method,
53
+ conversationId: innerConversationId,
54
+ },
55
+ };
56
+ }
57
+
58
+ return screenData;
59
+ }
60
+
61
+ router.all('/ui/:systemId/serve/:method/*', async (req: KapetaBodyRequest, res: Response) => {
62
+ readPageFromDisk(req.params.systemId, req.params[0], req.params.method, res);
63
+ });
64
+
65
+ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
36
66
  try {
37
67
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
68
+ const systemId = req.headers[SystemIdHeader.toLowerCase()] as string | undefined;
38
69
 
39
70
  const aiRequest: UIPagePrompt = JSON.parse(req.stringBody ?? '{}');
71
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
40
72
 
41
73
  const screenStream = await stormClient.createUIPage(aiRequest, conversationId);
42
74
 
@@ -48,12 +80,22 @@ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response)
48
80
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
49
81
  res.set(ConversationIdHeader, screenStream.getConversationId());
50
82
 
83
+ const promises: Promise<void>[] = [];
51
84
  screenStream.on('data', (data: StormEvent) => {
52
- console.log('Processing screen event', data);
85
+ switch (data.type) {
86
+ case 'PAGE':
87
+ console.log('Processing page event', data);
88
+ data.payload.conversationId = screenStream.getConversationId();
89
+ if (systemId) {
90
+ promises.push(sendPageEvent(systemId, data, res));
91
+ }
92
+ break;
93
+ }
53
94
  sendEvent(res, data);
54
95
  });
55
96
 
56
97
  await waitForStormStream(screenStream);
98
+ await Promise.allSettled(promises);
57
99
 
58
100
  sendDone(res);
59
101
  } catch (err: any) {
@@ -83,7 +125,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
83
125
 
84
126
  const promises: { [key: string]: Promise<void> } = {};
85
127
 
86
- const queue = new PromiseQueue(10);
128
+ const queue = new PromiseQueue(5);
87
129
  onRequestAborted(req, res, () => {
88
130
  queue.cancel();
89
131
  });
@@ -118,18 +160,23 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
118
160
  name: screen.name,
119
161
  title: screen.title,
120
162
  filename: screen.filename,
163
+ storage_prefix: userJourneysStream.getConversationId() + '_',
121
164
  },
122
165
  innerConversationId
123
166
  );
167
+ const promises: Promise<void>[] = [];
124
168
  screenStream.on('data', (screenData: StormEvent) => {
125
169
  if (screenData.type === 'PAGE') {
126
170
  screenData.payload.conversationId = innerConversationId;
171
+ promises.push(
172
+ sendPageEvent(userJourneysStream.getConversationId(), screenData, res)
173
+ );
174
+ } else {
175
+ sendEvent(res, screenData);
127
176
  }
128
-
129
- sendEvent(res, screenData);
130
177
  });
131
178
  screenStream.on('end', () => {
132
- resolve();
179
+ Promise.allSettled(promises).finally(resolve);
133
180
  });
134
181
  } catch (e: any) {
135
182
  console.error('Failed to process screen', e);
@@ -163,9 +210,37 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
163
210
  try {
164
211
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
165
212
 
166
- const aiRequest: StormContextRequest<UIPageEditPrompt> = JSON.parse(req.stringBody ?? '{}');
213
+ const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
214
+
215
+ const pages = aiRequest.prompt.pages
216
+ .map((page) => {
217
+ const content = readPageFromDiskAsString(conversationId!, page.path, page.method);
218
+ if (!content) {
219
+ console.warn('Page not found', page);
220
+ return undefined;
221
+ }
167
222
 
168
- const editStream = await stormClient.editPages(aiRequest.prompt, conversationId);
223
+ return {
224
+ filename: page.filename,
225
+ path: page.path,
226
+ method: page.method,
227
+ title: page.title,
228
+ conversationId: page.conversationId,
229
+ prompt: page.prompt,
230
+ name: page.name,
231
+ description: page.description,
232
+ content,
233
+ } satisfies Page;
234
+ })
235
+ .filter((page: Page | undefined) => !!page) as UIPageEditPrompt['pages'];
236
+
237
+ const editStream = await stormClient.editPages(
238
+ {
239
+ ...aiRequest.prompt,
240
+ pages,
241
+ },
242
+ conversationId
243
+ );
169
244
 
170
245
  onRequestAborted(req, res, () => {
171
246
  editStream.abort();
@@ -175,19 +250,24 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
175
250
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
176
251
  res.set(ConversationIdHeader, editStream.getConversationId());
177
252
 
253
+ const promises: Promise<void>[] = [];
178
254
  editStream.on('data', (data: StormEvent) => {
179
255
  try {
180
- sendEvent(res, data);
256
+ if (data.type === 'PAGE') {
257
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
258
+ } else {
259
+ sendEvent(res, data);
260
+ }
181
261
  } catch (e) {
182
262
  console.error('Failed to process event', e);
183
263
  }
184
264
  });
185
265
 
186
266
  await waitForStormStream(editStream);
187
-
188
267
  if (editStream.isAborted()) {
189
268
  return;
190
269
  }
270
+ await Promise.all(promises);
191
271
 
192
272
  sendDone(res);
193
273
  } catch (err: any) {
@@ -428,4 +508,14 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
428
508
  });
429
509
  }
430
510
 
511
+ function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
512
+ return writePageToDisk(mainConversationId, data)
513
+ .catch((err) => {
514
+ console.error('Failed to write page to disk', err);
515
+ })
516
+ .then(() => {
517
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
518
+ });
519
+ }
520
+
431
521
  export default router;
@@ -15,6 +15,7 @@ import {
15
15
  StormUIListPrompt,
16
16
  } from './stream';
17
17
  import { getRawAsset } from 'node:sea';
18
+ import { Page, StormEventPageUrl } from './events';
18
19
 
19
20
  export const STORM_ID = 'storm';
20
21
 
@@ -28,15 +29,20 @@ export interface UIPagePrompt {
28
29
  path: string;
29
30
  method: string;
30
31
  description: string;
32
+ storage_prefix: string;
31
33
  }
32
34
 
33
35
  export interface UIPageEditPrompt {
34
36
  planDescription: string;
35
37
  blockDescription: string;
36
- pages: {
37
- filename: string;
38
- content: string;
39
- }[];
38
+ pages: Page[];
39
+ prompt: string;
40
+ }
41
+
42
+ export interface UIPageEditRequest {
43
+ planDescription: string;
44
+ blockDescription: string;
45
+ pages: StormEventPageUrl['payload'][];
40
46
  prompt: string;
41
47
  }
42
48