@kapeta/local-cluster-service 0.58.6 → 0.60.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 +14 -0
- package/dist/cjs/src/clusterService.d.ts +1 -2
- package/dist/cjs/src/clusterService.js +14 -5
- package/dist/cjs/src/storm/UIServer.d.ts +11 -0
- package/dist/cjs/src/storm/UIServer.js +47 -0
- package/dist/cjs/src/storm/events.d.ts +20 -1
- package/dist/cjs/src/storm/page-utils.d.ts +12 -0
- package/dist/cjs/src/storm/page-utils.js +40 -0
- package/dist/cjs/src/storm/routes.js +111 -8
- package/dist/cjs/src/storm/stormClient.d.ts +9 -4
- package/dist/esm/src/clusterService.d.ts +1 -2
- package/dist/esm/src/clusterService.js +14 -5
- package/dist/esm/src/storm/UIServer.d.ts +11 -0
- package/dist/esm/src/storm/UIServer.js +47 -0
- package/dist/esm/src/storm/events.d.ts +20 -1
- package/dist/esm/src/storm/page-utils.d.ts +12 -0
- package/dist/esm/src/storm/page-utils.js +40 -0
- package/dist/esm/src/storm/routes.js +111 -8
- package/dist/esm/src/storm/stormClient.d.ts +9 -4
- package/package.json +1 -1
- package/src/clusterService.ts +14 -5
- package/src/storm/UIServer.ts +50 -0
- package/src/storm/events.ts +23 -1
- package/src/storm/page-utils.ts +53 -0
- package/src/storm/routes.ts +127 -13
- package/src/storm/stormClient.ts +10 -4
@@ -20,14 +20,48 @@ 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");
|
24
|
+
const UIServer_1 = require("./UIServer");
|
25
|
+
const UI_SERVERS = {};
|
23
26
|
const router = (0, express_promise_router_1.default)();
|
24
27
|
router.use('/', cors_1.corsHandler);
|
25
28
|
router.use('/', stringBody_1.stringBody);
|
26
|
-
|
27
|
-
|
29
|
+
function convertPageEvent(screenData, innerConversationId, mainConversationId) {
|
30
|
+
if (screenData.type === 'PAGE') {
|
31
|
+
const server = UI_SERVERS[mainConversationId];
|
32
|
+
if (!server) {
|
33
|
+
console.warn('No server found for conversation', mainConversationId);
|
34
|
+
}
|
35
|
+
screenData.payload.conversationId = innerConversationId;
|
36
|
+
return {
|
37
|
+
type: 'PAGE_URL',
|
38
|
+
reason: screenData.reason,
|
39
|
+
created: screenData.created,
|
40
|
+
payload: {
|
41
|
+
id: node_uuid_1.default.v4(),
|
42
|
+
name: screenData.payload.name,
|
43
|
+
title: screenData.payload.title,
|
44
|
+
filename: screenData.payload.filename,
|
45
|
+
description: screenData.payload.description,
|
46
|
+
prompt: screenData.payload.prompt,
|
47
|
+
path: screenData.payload.path,
|
48
|
+
url: server ? server.resolveUrl(screenData) : '',
|
49
|
+
method: screenData.payload.method,
|
50
|
+
conversationId: innerConversationId,
|
51
|
+
},
|
52
|
+
};
|
53
|
+
}
|
54
|
+
return screenData;
|
55
|
+
}
|
56
|
+
router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
|
57
|
+
(0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
|
58
|
+
});
|
59
|
+
router.post('/ui/screen', async (req, res) => {
|
28
60
|
try {
|
29
61
|
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
|
62
|
+
const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
|
30
63
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
64
|
+
aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
|
31
65
|
const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
|
32
66
|
onRequestAborted(req, res, () => {
|
33
67
|
screenStream.abort();
|
@@ -35,11 +69,21 @@ router.post('/:handle/ui/screen', async (req, res) => {
|
|
35
69
|
res.set('Content-Type', 'application/x-ndjson');
|
36
70
|
res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
|
37
71
|
res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
|
72
|
+
const promises = [];
|
38
73
|
screenStream.on('data', (data) => {
|
39
|
-
|
74
|
+
switch (data.type) {
|
75
|
+
case 'PAGE':
|
76
|
+
console.log('Processing page event', data);
|
77
|
+
data.payload.conversationId = screenStream.getConversationId();
|
78
|
+
if (systemId) {
|
79
|
+
promises.push(sendPageEvent(systemId, data, res));
|
80
|
+
}
|
81
|
+
break;
|
82
|
+
}
|
40
83
|
sendEvent(res, data);
|
41
84
|
});
|
42
85
|
await waitForStormStream(screenStream);
|
86
|
+
await Promise.allSettled(promises);
|
43
87
|
sendDone(res);
|
44
88
|
}
|
45
89
|
catch (err) {
|
@@ -49,6 +93,18 @@ router.post('/:handle/ui/screen', async (req, res) => {
|
|
49
93
|
}
|
50
94
|
}
|
51
95
|
});
|
96
|
+
router.delete('/:handle/ui', async (req, res) => {
|
97
|
+
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
|
98
|
+
if (!conversationId) {
|
99
|
+
res.status(400).send('Missing conversation id');
|
100
|
+
return;
|
101
|
+
}
|
102
|
+
const server = UI_SERVERS[conversationId];
|
103
|
+
if (server) {
|
104
|
+
server.close();
|
105
|
+
delete UI_SERVERS[conversationId];
|
106
|
+
}
|
107
|
+
});
|
52
108
|
router.post('/:handle/ui', async (req, res) => {
|
53
109
|
const handle = req.params.handle;
|
54
110
|
try {
|
@@ -62,10 +118,13 @@ router.post('/:handle/ui', async (req, res) => {
|
|
62
118
|
res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
|
63
119
|
res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
|
64
120
|
const promises = {};
|
65
|
-
const queue = new PromiseQueue_1.PromiseQueue(
|
121
|
+
const queue = new PromiseQueue_1.PromiseQueue(5);
|
66
122
|
onRequestAborted(req, res, () => {
|
67
123
|
queue.cancel();
|
68
124
|
});
|
125
|
+
const systemId = userJourneysStream.getConversationId();
|
126
|
+
UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
|
127
|
+
await UI_SERVERS[systemId].start();
|
69
128
|
userJourneysStream.on('data', async (data) => {
|
70
129
|
try {
|
71
130
|
console.log('Processing user journey event', data);
|
@@ -91,15 +150,20 @@ router.post('/:handle/ui', async (req, res) => {
|
|
91
150
|
name: screen.name,
|
92
151
|
title: screen.title,
|
93
152
|
filename: screen.filename,
|
153
|
+
storage_prefix: userJourneysStream.getConversationId() + '_',
|
94
154
|
}, innerConversationId);
|
155
|
+
const promises = [];
|
95
156
|
screenStream.on('data', (screenData) => {
|
96
157
|
if (screenData.type === 'PAGE') {
|
97
158
|
screenData.payload.conversationId = innerConversationId;
|
159
|
+
promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
|
160
|
+
}
|
161
|
+
else {
|
162
|
+
sendEvent(res, screenData);
|
98
163
|
}
|
99
|
-
sendEvent(res, screenData);
|
100
164
|
});
|
101
165
|
screenStream.on('end', () => {
|
102
|
-
|
166
|
+
Promise.allSettled(promises).finally(resolve);
|
103
167
|
});
|
104
168
|
}
|
105
169
|
catch (e) {
|
@@ -131,16 +195,45 @@ router.post('/ui/edit', async (req, res) => {
|
|
131
195
|
try {
|
132
196
|
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
|
133
197
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
134
|
-
const
|
198
|
+
const pages = aiRequest.prompt.pages
|
199
|
+
.map((page) => {
|
200
|
+
const content = (0, page_utils_1.readPageFromDiskAsString)(conversationId, page.path, page.method);
|
201
|
+
if (!content) {
|
202
|
+
console.warn('Page not found', page);
|
203
|
+
return undefined;
|
204
|
+
}
|
205
|
+
return {
|
206
|
+
filename: page.filename,
|
207
|
+
path: page.path,
|
208
|
+
method: page.method,
|
209
|
+
title: page.title,
|
210
|
+
conversationId: page.conversationId,
|
211
|
+
prompt: page.prompt,
|
212
|
+
name: page.name,
|
213
|
+
description: page.description,
|
214
|
+
content,
|
215
|
+
};
|
216
|
+
})
|
217
|
+
.filter((page) => !!page);
|
218
|
+
const editStream = await stormClient_1.stormClient.editPages({
|
219
|
+
...aiRequest.prompt,
|
220
|
+
pages,
|
221
|
+
}, conversationId);
|
135
222
|
onRequestAborted(req, res, () => {
|
136
223
|
editStream.abort();
|
137
224
|
});
|
138
225
|
res.set('Content-Type', 'application/x-ndjson');
|
139
226
|
res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
|
140
227
|
res.set(stormClient_1.ConversationIdHeader, editStream.getConversationId());
|
228
|
+
const promises = [];
|
141
229
|
editStream.on('data', (data) => {
|
142
230
|
try {
|
143
|
-
|
231
|
+
if (data.type === 'PAGE') {
|
232
|
+
promises.push(sendPageEvent(editStream.getConversationId(), data, res));
|
233
|
+
}
|
234
|
+
else {
|
235
|
+
sendEvent(res, data);
|
236
|
+
}
|
144
237
|
}
|
145
238
|
catch (e) {
|
146
239
|
console.error('Failed to process event', e);
|
@@ -150,6 +243,7 @@ router.post('/ui/edit', async (req, res) => {
|
|
150
243
|
if (editStream.isAborted()) {
|
151
244
|
return;
|
152
245
|
}
|
246
|
+
await Promise.all(promises);
|
153
247
|
sendDone(res);
|
154
248
|
}
|
155
249
|
catch (err) {
|
@@ -351,4 +445,13 @@ function onRequestAborted(req, res, onAborted) {
|
|
351
445
|
onAborted();
|
352
446
|
});
|
353
447
|
}
|
448
|
+
function sendPageEvent(mainConversationId, data, res) {
|
449
|
+
return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
|
450
|
+
.catch((err) => {
|
451
|
+
console.error('Failed to write page to disk', err);
|
452
|
+
})
|
453
|
+
.then(() => {
|
454
|
+
sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
|
455
|
+
});
|
456
|
+
}
|
354
457
|
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
|
-
|
18
|
-
|
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
package/src/clusterService.ts
CHANGED
@@ -56,15 +56,24 @@ class ClusterService {
|
|
56
56
|
|
57
57
|
/**
|
58
58
|
* Gets next available port
|
59
|
-
* @return {Promise<number>}
|
60
59
|
*/
|
61
|
-
async getNextAvailablePort() {
|
60
|
+
public async getNextAvailablePort(startPort: number = -1) {
|
61
|
+
let receivedStartPort = startPort > 0;
|
62
|
+
if (!receivedStartPort) {
|
63
|
+
startPort = this._currentPort;
|
64
|
+
}
|
62
65
|
while (true) {
|
63
|
-
while (this._reservedPorts.indexOf(
|
64
|
-
|
66
|
+
while (this._reservedPorts.indexOf(startPort) > -1) {
|
67
|
+
startPort++;
|
68
|
+
if (!receivedStartPort) {
|
69
|
+
this._currentPort = startPort;
|
70
|
+
}
|
65
71
|
}
|
66
72
|
|
67
|
-
const nextPort =
|
73
|
+
const nextPort = startPort++;
|
74
|
+
if (!receivedStartPort) {
|
75
|
+
this._currentPort = startPort;
|
76
|
+
}
|
68
77
|
const isUsed = await this._checkIfPortIsUsed(nextPort);
|
69
78
|
if (!isUsed) {
|
70
79
|
return nextPort;
|
@@ -0,0 +1,50 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
import express, { Express, Request, Response } from 'express';
|
6
|
+
import { readPageFromDisk } from './page-utils';
|
7
|
+
import { clusterService } from '../clusterService';
|
8
|
+
import { Server } from 'http';
|
9
|
+
import { StormEventPage } from './events';
|
10
|
+
|
11
|
+
export class UIServer {
|
12
|
+
private readonly express: Express;
|
13
|
+
private readonly systemId: string;
|
14
|
+
|
15
|
+
private port: number = 50000;
|
16
|
+
private server: Server | undefined;
|
17
|
+
|
18
|
+
constructor(systemId: string) {
|
19
|
+
this.systemId = systemId;
|
20
|
+
this.express = express();
|
21
|
+
}
|
22
|
+
|
23
|
+
public async start() {
|
24
|
+
this.port = await clusterService.getNextAvailablePort(this.port);
|
25
|
+
|
26
|
+
this.express.all('/*', async (req: Request, res: Response) => {
|
27
|
+
readPageFromDisk(this.systemId, req.params[0], req.method, res);
|
28
|
+
});
|
29
|
+
|
30
|
+
return new Promise<void>((resolve) => {
|
31
|
+
this.server = this.express.listen(this.port, () => {
|
32
|
+
console.log(`UI Server started on port ${this.port}`);
|
33
|
+
resolve();
|
34
|
+
});
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
public close() {
|
39
|
+
if (this.server) {
|
40
|
+
console.log('UI Server closed on port: %s', this.port);
|
41
|
+
this.server.close();
|
42
|
+
this.server = undefined;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
resolveUrl(screenData: StormEventPage) {
|
47
|
+
const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
|
48
|
+
return `http://localhost:${this.port}${path}`;
|
49
|
+
}
|
50
|
+
}
|
package/src/storm/events.ts
CHANGED
@@ -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
|
+
}
|
package/src/storm/routes.ts
CHANGED
@@ -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,55 @@ 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';
|
29
|
+
import { UIServer } from './UIServer';
|
28
30
|
|
31
|
+
const UI_SERVERS: { [key: string]: UIServer } = {};
|
29
32
|
const router = Router();
|
30
33
|
|
31
34
|
router.use('/', corsHandler);
|
32
35
|
router.use('/', stringBody);
|
33
36
|
|
34
|
-
|
35
|
-
|
37
|
+
function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
|
38
|
+
if (screenData.type === 'PAGE') {
|
39
|
+
const server: UIServer | undefined = UI_SERVERS[mainConversationId];
|
40
|
+
if (!server) {
|
41
|
+
console.warn('No server found for conversation', mainConversationId);
|
42
|
+
}
|
43
|
+
screenData.payload.conversationId = innerConversationId;
|
44
|
+
return {
|
45
|
+
type: 'PAGE_URL',
|
46
|
+
reason: screenData.reason,
|
47
|
+
created: screenData.created,
|
48
|
+
payload: {
|
49
|
+
id: uuid.v4(),
|
50
|
+
name: screenData.payload.name,
|
51
|
+
title: screenData.payload.title,
|
52
|
+
filename: screenData.payload.filename,
|
53
|
+
description: screenData.payload.description,
|
54
|
+
prompt: screenData.payload.prompt,
|
55
|
+
path: screenData.payload.path,
|
56
|
+
url: server ? server.resolveUrl(screenData) : '',
|
57
|
+
method: screenData.payload.method,
|
58
|
+
conversationId: innerConversationId,
|
59
|
+
},
|
60
|
+
};
|
61
|
+
}
|
62
|
+
|
63
|
+
return screenData;
|
64
|
+
}
|
65
|
+
|
66
|
+
router.all('/ui/:systemId/serve/:method/*', async (req: KapetaBodyRequest, res: Response) => {
|
67
|
+
readPageFromDisk(req.params.systemId, req.params[0], req.params.method, res);
|
68
|
+
});
|
69
|
+
|
70
|
+
router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
36
71
|
try {
|
37
72
|
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
73
|
+
const systemId = req.headers[SystemIdHeader.toLowerCase()] as string | undefined;
|
38
74
|
|
39
75
|
const aiRequest: UIPagePrompt = JSON.parse(req.stringBody ?? '{}');
|
76
|
+
aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
|
40
77
|
|
41
78
|
const screenStream = await stormClient.createUIPage(aiRequest, conversationId);
|
42
79
|
|
@@ -48,12 +85,22 @@ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response)
|
|
48
85
|
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
49
86
|
res.set(ConversationIdHeader, screenStream.getConversationId());
|
50
87
|
|
88
|
+
const promises: Promise<void>[] = [];
|
51
89
|
screenStream.on('data', (data: StormEvent) => {
|
52
|
-
|
90
|
+
switch (data.type) {
|
91
|
+
case 'PAGE':
|
92
|
+
console.log('Processing page event', data);
|
93
|
+
data.payload.conversationId = screenStream.getConversationId();
|
94
|
+
if (systemId) {
|
95
|
+
promises.push(sendPageEvent(systemId, data, res));
|
96
|
+
}
|
97
|
+
break;
|
98
|
+
}
|
53
99
|
sendEvent(res, data);
|
54
100
|
});
|
55
101
|
|
56
102
|
await waitForStormStream(screenStream);
|
103
|
+
await Promise.allSettled(promises);
|
57
104
|
|
58
105
|
sendDone(res);
|
59
106
|
} catch (err: any) {
|
@@ -64,6 +111,20 @@ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response)
|
|
64
111
|
}
|
65
112
|
});
|
66
113
|
|
114
|
+
router.delete('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
115
|
+
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
116
|
+
if (!conversationId) {
|
117
|
+
res.status(400).send('Missing conversation id');
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
|
121
|
+
const server = UI_SERVERS[conversationId];
|
122
|
+
if (server) {
|
123
|
+
server.close();
|
124
|
+
delete UI_SERVERS[conversationId];
|
125
|
+
}
|
126
|
+
});
|
127
|
+
|
67
128
|
router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
68
129
|
const handle = req.params.handle as string;
|
69
130
|
try {
|
@@ -83,11 +144,16 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
83
144
|
|
84
145
|
const promises: { [key: string]: Promise<void> } = {};
|
85
146
|
|
86
|
-
const queue = new PromiseQueue(
|
147
|
+
const queue = new PromiseQueue(5);
|
87
148
|
onRequestAborted(req, res, () => {
|
88
149
|
queue.cancel();
|
89
150
|
});
|
90
151
|
|
152
|
+
const systemId = userJourneysStream.getConversationId();
|
153
|
+
|
154
|
+
UI_SERVERS[systemId] = new UIServer(systemId);
|
155
|
+
await UI_SERVERS[systemId].start();
|
156
|
+
|
91
157
|
userJourneysStream.on('data', async (data: StormEvent) => {
|
92
158
|
try {
|
93
159
|
console.log('Processing user journey event', data);
|
@@ -118,18 +184,23 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
118
184
|
name: screen.name,
|
119
185
|
title: screen.title,
|
120
186
|
filename: screen.filename,
|
187
|
+
storage_prefix: userJourneysStream.getConversationId() + '_',
|
121
188
|
},
|
122
189
|
innerConversationId
|
123
190
|
);
|
191
|
+
const promises: Promise<void>[] = [];
|
124
192
|
screenStream.on('data', (screenData: StormEvent) => {
|
125
193
|
if (screenData.type === 'PAGE') {
|
126
194
|
screenData.payload.conversationId = innerConversationId;
|
195
|
+
promises.push(
|
196
|
+
sendPageEvent(userJourneysStream.getConversationId(), screenData, res)
|
197
|
+
);
|
198
|
+
} else {
|
199
|
+
sendEvent(res, screenData);
|
127
200
|
}
|
128
|
-
|
129
|
-
sendEvent(res, screenData);
|
130
201
|
});
|
131
202
|
screenStream.on('end', () => {
|
132
|
-
|
203
|
+
Promise.allSettled(promises).finally(resolve);
|
133
204
|
});
|
134
205
|
} catch (e: any) {
|
135
206
|
console.error('Failed to process screen', e);
|
@@ -163,9 +234,37 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
|
|
163
234
|
try {
|
164
235
|
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
165
236
|
|
166
|
-
const aiRequest: StormContextRequest<
|
237
|
+
const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
|
238
|
+
|
239
|
+
const pages = aiRequest.prompt.pages
|
240
|
+
.map((page) => {
|
241
|
+
const content = readPageFromDiskAsString(conversationId!, page.path, page.method);
|
242
|
+
if (!content) {
|
243
|
+
console.warn('Page not found', page);
|
244
|
+
return undefined;
|
245
|
+
}
|
167
246
|
|
168
|
-
|
247
|
+
return {
|
248
|
+
filename: page.filename,
|
249
|
+
path: page.path,
|
250
|
+
method: page.method,
|
251
|
+
title: page.title,
|
252
|
+
conversationId: page.conversationId,
|
253
|
+
prompt: page.prompt,
|
254
|
+
name: page.name,
|
255
|
+
description: page.description,
|
256
|
+
content,
|
257
|
+
} satisfies Page;
|
258
|
+
})
|
259
|
+
.filter((page: Page | undefined) => !!page) as UIPageEditPrompt['pages'];
|
260
|
+
|
261
|
+
const editStream = await stormClient.editPages(
|
262
|
+
{
|
263
|
+
...aiRequest.prompt,
|
264
|
+
pages,
|
265
|
+
},
|
266
|
+
conversationId
|
267
|
+
);
|
169
268
|
|
170
269
|
onRequestAborted(req, res, () => {
|
171
270
|
editStream.abort();
|
@@ -175,19 +274,24 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
|
|
175
274
|
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
176
275
|
res.set(ConversationIdHeader, editStream.getConversationId());
|
177
276
|
|
277
|
+
const promises: Promise<void>[] = [];
|
178
278
|
editStream.on('data', (data: StormEvent) => {
|
179
279
|
try {
|
180
|
-
|
280
|
+
if (data.type === 'PAGE') {
|
281
|
+
promises.push(sendPageEvent(editStream.getConversationId(), data, res));
|
282
|
+
} else {
|
283
|
+
sendEvent(res, data);
|
284
|
+
}
|
181
285
|
} catch (e) {
|
182
286
|
console.error('Failed to process event', e);
|
183
287
|
}
|
184
288
|
});
|
185
289
|
|
186
290
|
await waitForStormStream(editStream);
|
187
|
-
|
188
291
|
if (editStream.isAborted()) {
|
189
292
|
return;
|
190
293
|
}
|
294
|
+
await Promise.all(promises);
|
191
295
|
|
192
296
|
sendDone(res);
|
193
297
|
} catch (err: any) {
|
@@ -428,4 +532,14 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
|
|
428
532
|
});
|
429
533
|
}
|
430
534
|
|
535
|
+
function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
|
536
|
+
return writePageToDisk(mainConversationId, data)
|
537
|
+
.catch((err) => {
|
538
|
+
console.error('Failed to write page to disk', err);
|
539
|
+
})
|
540
|
+
.then(() => {
|
541
|
+
sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
|
542
|
+
});
|
543
|
+
}
|
544
|
+
|
431
545
|
export default router;
|