@kapeta/local-cluster-service 0.61.2 → 0.62.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 +7 -0
- package/dist/cjs/src/storm/PageGenerator.d.ts +39 -0
- package/dist/cjs/src/storm/PageGenerator.js +143 -0
- package/dist/cjs/src/storm/events.d.ts +30 -1
- package/dist/cjs/src/storm/page-utils.d.ts +10 -0
- package/dist/cjs/src/storm/page-utils.js +65 -6
- package/dist/cjs/src/storm/routes.js +104 -56
- package/dist/cjs/src/storm/stormClient.d.ts +7 -1
- package/dist/cjs/src/storm/stormClient.js +24 -1
- package/dist/cjs/src/storm/stream.d.ts +1 -0
- package/dist/cjs/src/storm/stream.js +1 -0
- package/dist/esm/src/storm/PageGenerator.d.ts +39 -0
- package/dist/esm/src/storm/PageGenerator.js +143 -0
- package/dist/esm/src/storm/events.d.ts +30 -1
- package/dist/esm/src/storm/page-utils.d.ts +10 -0
- package/dist/esm/src/storm/page-utils.js +65 -6
- package/dist/esm/src/storm/routes.js +104 -56
- package/dist/esm/src/storm/stormClient.d.ts +7 -1
- package/dist/esm/src/storm/stormClient.js +24 -1
- package/dist/esm/src/storm/stream.d.ts +1 -0
- package/dist/esm/src/storm/stream.js +1 -0
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +181 -0
- package/src/storm/events.ts +37 -1
- package/src/storm/page-utils.ts +88 -12
- package/src/storm/routes.ts +146 -70
- package/src/storm/stormClient.ts +33 -1
- package/src/storm/stream.ts +2 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
|
6
|
+
import uuid from 'node-uuid';
|
7
|
+
import { stormClient, UIPagePrompt } from './stormClient';
|
8
|
+
import { ReferenceClassification, StormEvent, StormEventPage, StormEventReferenceClassification } from './events';
|
9
|
+
import { EventEmitter } from 'node:events';
|
10
|
+
import { PromiseQueue } from './PromiseQueue';
|
11
|
+
|
12
|
+
export class PageQueue extends EventEmitter {
|
13
|
+
private readonly queue: PromiseQueue;
|
14
|
+
private readonly systemId: string;
|
15
|
+
private readonly references: Map<string, PageGenerator> = new Map();
|
16
|
+
|
17
|
+
constructor(systemId: string, concurrency: number = 5) {
|
18
|
+
super();
|
19
|
+
this.systemId = systemId;
|
20
|
+
this.queue = new PromiseQueue(concurrency);
|
21
|
+
}
|
22
|
+
|
23
|
+
on(event: 'event', listener: (data: StormEvent) => void): this;
|
24
|
+
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
25
|
+
|
26
|
+
on(event: string, listener: (...args: any[]) => void): this {
|
27
|
+
return super.on(event, listener);
|
28
|
+
}
|
29
|
+
|
30
|
+
emit(type: 'event', event: StormEvent): boolean;
|
31
|
+
emit(type: 'page', event: StormEventPage): boolean;
|
32
|
+
emit(eventName: string | symbol, ...args: any[]): boolean {
|
33
|
+
return super.emit(eventName, ...args);
|
34
|
+
}
|
35
|
+
|
36
|
+
public addPrompt(initialPrompt: UIPagePrompt) {
|
37
|
+
if (this.references.has(initialPrompt.path)) {
|
38
|
+
console.log('Ignoring duplicate prompt', initialPrompt.path);
|
39
|
+
return Promise.resolve();
|
40
|
+
}
|
41
|
+
console.log('processing prompt', initialPrompt.path);
|
42
|
+
const generator = new PageGenerator(initialPrompt);
|
43
|
+
this.references.set(initialPrompt.path, generator);
|
44
|
+
return this.addPageGenerator(generator);
|
45
|
+
}
|
46
|
+
|
47
|
+
private async addPageGenerator(generator: PageGenerator) {
|
48
|
+
generator.on('event', (event: StormEvent) => this.emit('event', event));
|
49
|
+
generator.on('page_refs', ({ event, references }) => {
|
50
|
+
this.emit('page', event);
|
51
|
+
references.forEach((reference) => {
|
52
|
+
if (
|
53
|
+
reference.url.startsWith('#') ||
|
54
|
+
reference.url.startsWith('javascript:') ||
|
55
|
+
reference.url.startsWith('http://') ||
|
56
|
+
reference.url.startsWith('https://')
|
57
|
+
) {
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
|
61
|
+
switch (reference.type) {
|
62
|
+
case 'image':
|
63
|
+
console.log('Ignoring image reference', reference);
|
64
|
+
break;
|
65
|
+
case 'css':
|
66
|
+
case 'javascript':
|
67
|
+
//console.log('Ignoring reference', reference);
|
68
|
+
break;
|
69
|
+
case 'html':
|
70
|
+
console.log('Adding page generator for', reference);
|
71
|
+
const paths = Array.from(this.references.keys());
|
72
|
+
this.addPrompt({
|
73
|
+
name: reference.name,
|
74
|
+
title: reference.title,
|
75
|
+
path: reference.url,
|
76
|
+
method: 'GET',
|
77
|
+
storage_prefix: this.systemId + '_',
|
78
|
+
prompt:
|
79
|
+
`Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
|
80
|
+
`The page was referenced from this page: \`\`\`html\n${event.payload.content}\n\`\`\`\n` +
|
81
|
+
(paths.length > 0
|
82
|
+
? `\nThese paths are already implemented:\n- ${paths.join('\n - ')}\n\n`
|
83
|
+
: ''),
|
84
|
+
description: reference.description,
|
85
|
+
filename: '',
|
86
|
+
});
|
87
|
+
break;
|
88
|
+
}
|
89
|
+
});
|
90
|
+
});
|
91
|
+
return this.queue.add(() => generator.generate());
|
92
|
+
}
|
93
|
+
|
94
|
+
public cancel() {
|
95
|
+
this.queue.cancel();
|
96
|
+
}
|
97
|
+
|
98
|
+
public wait() {
|
99
|
+
return this.queue.wait();
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
export class PageGenerator extends EventEmitter {
|
104
|
+
private readonly conversationId: string;
|
105
|
+
private prompt: UIPagePrompt;
|
106
|
+
|
107
|
+
constructor(prompt: UIPagePrompt, conversationId: string = uuid.v4()) {
|
108
|
+
super();
|
109
|
+
this.conversationId = conversationId;
|
110
|
+
this.prompt = prompt;
|
111
|
+
}
|
112
|
+
|
113
|
+
on(event: 'event', listener: (data: StormEvent) => void): this;
|
114
|
+
on(
|
115
|
+
event: 'page_refs',
|
116
|
+
listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => void
|
117
|
+
): this;
|
118
|
+
|
119
|
+
on(event: string, listener: (...args: any[]) => void): this {
|
120
|
+
return super.on(event, listener);
|
121
|
+
}
|
122
|
+
|
123
|
+
emit(type: 'event', event: StormEvent): boolean;
|
124
|
+
emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
|
125
|
+
emit(eventName: string | symbol, ...args: any[]): boolean {
|
126
|
+
return super.emit(eventName, ...args);
|
127
|
+
}
|
128
|
+
|
129
|
+
public async generate() {
|
130
|
+
return new Promise<void>(async (resolve) => {
|
131
|
+
const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
|
132
|
+
|
133
|
+
const promises: Promise<void>[] = [];
|
134
|
+
|
135
|
+
screenStream.on('data', (event: StormEvent) => {
|
136
|
+
if (event.type === 'PAGE') {
|
137
|
+
event.payload.conversationId = this.conversationId;
|
138
|
+
|
139
|
+
promises.push(
|
140
|
+
(async () => {
|
141
|
+
const references = await this.resolveReferences(event.payload.content);
|
142
|
+
//console.log('Resolved references for page', references, event.payload);
|
143
|
+
this.emit('page_refs', {
|
144
|
+
event,
|
145
|
+
references,
|
146
|
+
});
|
147
|
+
})()
|
148
|
+
);
|
149
|
+
return;
|
150
|
+
}
|
151
|
+
|
152
|
+
this.emit('event', event);
|
153
|
+
});
|
154
|
+
|
155
|
+
screenStream.on('end', () => {
|
156
|
+
Promise.allSettled(promises).finally(resolve);
|
157
|
+
});
|
158
|
+
|
159
|
+
await screenStream.waitForDone();
|
160
|
+
});
|
161
|
+
}
|
162
|
+
|
163
|
+
private async resolveReferences(content: string) {
|
164
|
+
const referenceStream = await stormClient.classifyUIReferences(content);
|
165
|
+
|
166
|
+
const references: ReferenceClassification[] = [];
|
167
|
+
|
168
|
+
referenceStream.on('data', (referenceData: StormEvent) => {
|
169
|
+
if (referenceData.type !== 'REF_CLASSIFICATION') {
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
|
173
|
+
//console.log('Processing reference classification', referenceData);
|
174
|
+
references.push(referenceData.payload);
|
175
|
+
});
|
176
|
+
|
177
|
+
await referenceStream.waitForDone();
|
178
|
+
|
179
|
+
return references;
|
180
|
+
}
|
181
|
+
}
|
package/src/storm/events.ts
CHANGED
@@ -406,6 +406,40 @@ export interface StormEventPromptImprove {
|
|
406
406
|
};
|
407
407
|
}
|
408
408
|
|
409
|
+
export interface LandingPage {
|
410
|
+
name: string;
|
411
|
+
title: string;
|
412
|
+
filename: string;
|
413
|
+
create_prompt: string;
|
414
|
+
path: string;
|
415
|
+
archetype: string;
|
416
|
+
requires_authentication: boolean;
|
417
|
+
}
|
418
|
+
|
419
|
+
// Event for defining a landing pages
|
420
|
+
export interface StormEventLandingPage {
|
421
|
+
type: 'LANDING_PAGE';
|
422
|
+
reason: string;
|
423
|
+
created: number;
|
424
|
+
payload: LandingPage;
|
425
|
+
}
|
426
|
+
|
427
|
+
export interface ReferenceClassification {
|
428
|
+
name: string;
|
429
|
+
title: string;
|
430
|
+
url: string;
|
431
|
+
description: string;
|
432
|
+
type: 'image' | 'css' | 'javascript' | 'html';
|
433
|
+
source: 'local' | 'cdn' | 'example';
|
434
|
+
}
|
435
|
+
// Event for reference classification
|
436
|
+
export interface StormEventReferenceClassification {
|
437
|
+
type: 'REF_CLASSIFICATION';
|
438
|
+
reason: string;
|
439
|
+
created: number;
|
440
|
+
payload: ReferenceClassification;
|
441
|
+
}
|
442
|
+
|
409
443
|
export type StormEvent =
|
410
444
|
| StormEventCreateBlock
|
411
445
|
| StormEventCreateConnection
|
@@ -435,4 +469,6 @@ export type StormEvent =
|
|
435
469
|
| StormEventUIShell
|
436
470
|
| StormEventPage
|
437
471
|
| StormEventPageUrl
|
438
|
-
| StormEventPromptImprove
|
472
|
+
| StormEventPromptImprove
|
473
|
+
| StormEventLandingPage
|
474
|
+
| StormEventReferenceClassification;
|
package/src/storm/page-utils.ts
CHANGED
@@ -7,18 +7,23 @@ import { Response } from 'express';
|
|
7
7
|
import os from 'node:os';
|
8
8
|
import Path from 'path';
|
9
9
|
import FS from 'fs-extra';
|
10
|
+
import FSExtra from 'fs-extra';
|
11
|
+
import { ConversationItem } from './stream';
|
12
|
+
import exp from 'node:constants';
|
10
13
|
|
11
14
|
export const SystemIdHeader = 'System-Id';
|
12
15
|
|
16
|
+
function normalizePath(path: string) {
|
17
|
+
return path
|
18
|
+
.replace(/\?.*$/gi, '')
|
19
|
+
.replace(/:[a-z][a-z_]*\b/gi, '*')
|
20
|
+
.replace(/\{[a-z]+}/gi, '*');
|
21
|
+
}
|
22
|
+
|
13
23
|
export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
14
|
-
const
|
15
|
-
|
16
|
-
|
17
|
-
systemId,
|
18
|
-
event.payload.path,
|
19
|
-
event.payload.method.toLowerCase(),
|
20
|
-
'index.html'
|
21
|
-
);
|
24
|
+
const baseDir = getBaseDir(systemId);
|
25
|
+
const filePath = getFilePath(event.payload.method);
|
26
|
+
const path = Path.join(baseDir, normalizePath(event.payload.path), filePath);
|
22
27
|
await FS.ensureDir(Path.dirname(path));
|
23
28
|
await FS.writeFile(path, event.payload.content);
|
24
29
|
|
@@ -29,9 +34,58 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
|
29
34
|
};
|
30
35
|
}
|
31
36
|
|
37
|
+
function getBaseDir(systemId: string) {
|
38
|
+
return Path.join(os.tmpdir(), 'ai-systems', systemId);
|
39
|
+
}
|
40
|
+
|
41
|
+
function getFilePath(method: string) {
|
42
|
+
return Path.join(method.toLowerCase(), 'index.html');
|
43
|
+
}
|
44
|
+
|
45
|
+
export function resolveReadPath(systemId: string, path: string, method: string) {
|
46
|
+
const baseDir = getBaseDir(systemId);
|
47
|
+
|
48
|
+
path = normalizePath(path);
|
49
|
+
|
50
|
+
const filePath = getFilePath(method);
|
51
|
+
|
52
|
+
const fullPath = Path.join(baseDir, path, filePath);
|
53
|
+
|
54
|
+
if (FS.existsSync(fullPath)) {
|
55
|
+
return fullPath;
|
56
|
+
}
|
57
|
+
|
58
|
+
const parts = path.split(/\*/g);
|
59
|
+
|
60
|
+
let currentPath = '';
|
61
|
+
|
62
|
+
for (let part in parts) {
|
63
|
+
const thisPath = Path.join(currentPath, part);
|
64
|
+
const starPath = Path.join(currentPath, '*');
|
65
|
+
const thisPathDir = Path.join(baseDir, thisPath);
|
66
|
+
const starPathDir = Path.join(baseDir, starPath);
|
67
|
+
|
68
|
+
if (FS.existsSync(thisPathDir)) {
|
69
|
+
currentPath = thisPath;
|
70
|
+
continue;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (FS.existsSync(starPathDir)) {
|
74
|
+
currentPath = starPath;
|
75
|
+
continue;
|
76
|
+
}
|
77
|
+
|
78
|
+
console.log('Path not found', thisPathDir, starPathDir);
|
79
|
+
// Path not found
|
80
|
+
return null;
|
81
|
+
}
|
82
|
+
|
83
|
+
return Path.join(baseDir, currentPath, filePath);
|
84
|
+
}
|
85
|
+
|
32
86
|
export function readPageFromDiskAsString(systemId: string, path: string, method: string) {
|
33
|
-
const filePath =
|
34
|
-
if (!FS.existsSync(filePath)) {
|
87
|
+
const filePath = resolveReadPath(systemId, path, method);
|
88
|
+
if (!filePath || !FS.existsSync(filePath)) {
|
35
89
|
return null;
|
36
90
|
}
|
37
91
|
|
@@ -39,8 +93,8 @@ export function readPageFromDiskAsString(systemId: string, path: string, method:
|
|
39
93
|
}
|
40
94
|
|
41
95
|
export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
|
42
|
-
const filePath =
|
43
|
-
if (!FS.existsSync(filePath)) {
|
96
|
+
const filePath = resolveReadPath(systemId, path, method);
|
97
|
+
if (!filePath || !FS.existsSync(filePath)) {
|
44
98
|
res.status(404).send('Page not found');
|
45
99
|
return;
|
46
100
|
}
|
@@ -51,3 +105,25 @@ export function readPageFromDisk(systemId: string, path: string, method: string,
|
|
51
105
|
res.write(content);
|
52
106
|
res.end();
|
53
107
|
}
|
108
|
+
|
109
|
+
export interface Conversation {
|
110
|
+
messages: ConversationItem[];
|
111
|
+
variantId: string;
|
112
|
+
type: 'page';
|
113
|
+
filename: string;
|
114
|
+
}
|
115
|
+
|
116
|
+
export function readConversationFromFile(filename: string): Conversation[] {
|
117
|
+
if (!FS.existsSync(filename)) {
|
118
|
+
return [];
|
119
|
+
}
|
120
|
+
const content = FS.readFileSync(filename).toString();
|
121
|
+
if (!content.trim()) {
|
122
|
+
return [];
|
123
|
+
}
|
124
|
+
return content.split(/\n/g).map((line) => JSON.parse(line) as Conversation);
|
125
|
+
}
|
126
|
+
|
127
|
+
export function writeConversationToFile(filename: string, conversations: Conversation[]) {
|
128
|
+
FS.writeFileSync(filename, conversations.map((conversation) => JSON.stringify(conversation)).join('\n'));
|
129
|
+
}
|
package/src/storm/routes.ts
CHANGED
@@ -12,8 +12,17 @@ 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
|
-
|
15
|
+
|
16
|
+
import {
|
17
|
+
ConversationIdHeader,
|
18
|
+
stormClient,
|
19
|
+
UIPagePrompt,
|
20
|
+
UIPageEditPrompt,
|
21
|
+
UIPageEditRequest,
|
22
|
+
UIPageSamplePrompt,
|
23
|
+
} from './stormClient';
|
16
24
|
import { Page, StormEvent, StormEventPage, StormEventPhaseType, UIShell, UserJourneyScreen } from './events';
|
25
|
+
|
17
26
|
import {
|
18
27
|
createPhaseEndEvent,
|
19
28
|
createPhaseStartEvent,
|
@@ -25,8 +34,17 @@ import { StormCodegen } from './codegen';
|
|
25
34
|
import { assetManager } from '../assetManager';
|
26
35
|
import uuid from 'node-uuid';
|
27
36
|
import { PromiseQueue } from './PromiseQueue';
|
28
|
-
import {
|
37
|
+
import {
|
38
|
+
readConversationFromFile,
|
39
|
+
readPageFromDisk,
|
40
|
+
readPageFromDiskAsString,
|
41
|
+
SystemIdHeader,
|
42
|
+
writeConversationToFile,
|
43
|
+
writePageToDisk,
|
44
|
+
} from './page-utils';
|
29
45
|
import { UIServer } from './UIServer';
|
46
|
+
import { PageQueue } from './PageGenerator';
|
47
|
+
import FSExtra from 'fs-extra';
|
30
48
|
|
31
49
|
const UI_SERVERS: { [key: string]: UIServer } = {};
|
32
50
|
const router = Router();
|
@@ -34,6 +52,8 @@ const router = Router();
|
|
34
52
|
router.use('/', corsHandler);
|
35
53
|
router.use('/', stringBody);
|
36
54
|
|
55
|
+
const samplesBaseDir = Path.join(__dirname, 'samples');
|
56
|
+
|
37
57
|
function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
|
38
58
|
if (screenData.type === 'PAGE') {
|
39
59
|
const server: UIServer | undefined = UI_SERVERS[mainConversationId];
|
@@ -75,22 +95,25 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
75
95
|
const aiRequest: UIPagePrompt = JSON.parse(req.stringBody ?? '{}');
|
76
96
|
aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
|
77
97
|
|
78
|
-
|
98
|
+
res.set('Content-Type', 'application/x-ndjson');
|
99
|
+
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
100
|
+
res.set(ConversationIdHeader, conversationId);
|
101
|
+
|
102
|
+
const parentConversationId = systemId ?? '';
|
79
103
|
|
104
|
+
const queue = new PageQueue(parentConversationId, 5);
|
80
105
|
onRequestAborted(req, res, () => {
|
81
|
-
|
106
|
+
queue.cancel();
|
82
107
|
});
|
83
108
|
|
84
|
-
|
85
|
-
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
86
|
-
res.set(ConversationIdHeader, screenStream.getConversationId());
|
109
|
+
await queue.addPrompt(aiRequest);
|
87
110
|
|
88
111
|
const promises: Promise<void>[] = [];
|
89
|
-
|
112
|
+
queue.on('page', (data) => {
|
90
113
|
switch (data.type) {
|
91
114
|
case 'PAGE':
|
92
115
|
console.log('Processing page event', data);
|
93
|
-
|
116
|
+
|
94
117
|
if (systemId) {
|
95
118
|
promises.push(sendPageEvent(systemId, data, res));
|
96
119
|
}
|
@@ -99,7 +122,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
99
122
|
sendEvent(res, data);
|
100
123
|
});
|
101
124
|
|
102
|
-
await
|
125
|
+
await queue.wait();
|
103
126
|
await Promise.allSettled(promises);
|
104
127
|
|
105
128
|
sendDone(res);
|
@@ -125,6 +148,93 @@ router.delete('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
125
148
|
}
|
126
149
|
});
|
127
150
|
|
151
|
+
router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Response) => {
|
152
|
+
const handle = req.params.handle as string;
|
153
|
+
try {
|
154
|
+
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
155
|
+
|
156
|
+
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
157
|
+
|
158
|
+
const landingPagesStream = await stormClient.createUILandingPages(aiRequest.prompt, conversationId);
|
159
|
+
|
160
|
+
onRequestAborted(req, res, () => {
|
161
|
+
landingPagesStream.abort();
|
162
|
+
});
|
163
|
+
|
164
|
+
res.set('Content-Type', 'application/x-ndjson');
|
165
|
+
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
166
|
+
|
167
|
+
res.set(ConversationIdHeader, landingPagesStream.getConversationId());
|
168
|
+
|
169
|
+
const promises: { [key: string]: Promise<void> } = {};
|
170
|
+
const pageEventPromises: Promise<void>[] = [];
|
171
|
+
const systemId = landingPagesStream.getConversationId();
|
172
|
+
const pageQueue = new PageQueue(systemId, 5);
|
173
|
+
|
174
|
+
landingPagesStream.on('data', async (data: StormEvent) => {
|
175
|
+
try {
|
176
|
+
sendEvent(res, data);
|
177
|
+
if (data.type !== 'LANDING_PAGE') {
|
178
|
+
return;
|
179
|
+
}
|
180
|
+
|
181
|
+
if (landingPagesStream.isAborted()) {
|
182
|
+
return;
|
183
|
+
}
|
184
|
+
const landingPage = data.payload;
|
185
|
+
if (landingPage.name in promises) {
|
186
|
+
return;
|
187
|
+
}
|
188
|
+
|
189
|
+
// We add the landing pages to the prompt queue.
|
190
|
+
// These will then be analysed - creating further pages as needed
|
191
|
+
promises[landingPage.name] = pageQueue.addPrompt({
|
192
|
+
prompt: landingPage.create_prompt,
|
193
|
+
method: 'GET',
|
194
|
+
path: landingPage.path,
|
195
|
+
description: landingPage.create_prompt,
|
196
|
+
name: landingPage.name,
|
197
|
+
title: landingPage.title,
|
198
|
+
filename: landingPage.filename,
|
199
|
+
storage_prefix: systemId + '_',
|
200
|
+
});
|
201
|
+
} catch (e) {
|
202
|
+
console.error('Failed to process event', e);
|
203
|
+
}
|
204
|
+
});
|
205
|
+
|
206
|
+
UI_SERVERS[systemId] = new UIServer(systemId);
|
207
|
+
await UI_SERVERS[systemId].start();
|
208
|
+
|
209
|
+
onRequestAborted(req, res, () => {
|
210
|
+
pageQueue.cancel();
|
211
|
+
});
|
212
|
+
|
213
|
+
pageQueue.on('page', (screenData: StormEventPage) => {
|
214
|
+
pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
|
215
|
+
});
|
216
|
+
|
217
|
+
pageQueue.on('event', (screenData: StormEvent) => {
|
218
|
+
sendEvent(res, screenData);
|
219
|
+
});
|
220
|
+
|
221
|
+
await waitForStormStream(landingPagesStream);
|
222
|
+
await pageQueue.wait();
|
223
|
+
await Promise.allSettled(pageEventPromises);
|
224
|
+
|
225
|
+
if (landingPagesStream.isAborted()) {
|
226
|
+
return;
|
227
|
+
}
|
228
|
+
|
229
|
+
sendDone(res);
|
230
|
+
} catch (err) {
|
231
|
+
sendError(err as Error, res);
|
232
|
+
if (!res.closed) {
|
233
|
+
res.end();
|
234
|
+
}
|
235
|
+
}
|
236
|
+
});
|
237
|
+
|
128
238
|
router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
129
239
|
const handle = req.params.handle as string;
|
130
240
|
try {
|
@@ -223,75 +333,40 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
223
333
|
await UI_SERVERS[outerConversationId].start();
|
224
334
|
|
225
335
|
// Get the pages (5 at a time)
|
226
|
-
const queue = new
|
227
|
-
|
336
|
+
const queue = new PageQueue(outerConversationId, 5);
|
337
|
+
const pagePromises: Promise<void>[] = [];
|
228
338
|
onRequestAborted(req, res, () => {
|
229
339
|
queue.cancel();
|
230
340
|
});
|
231
341
|
|
342
|
+
const pageEventPromises: Promise<void>[] = [];
|
343
|
+
queue.on('page', (screenData: StormEventPage) => {
|
344
|
+
pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
|
345
|
+
});
|
346
|
+
|
347
|
+
queue.on('event', (screenData: StormEvent) => {
|
348
|
+
sendEvent(res, screenData);
|
349
|
+
});
|
350
|
+
|
232
351
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
233
|
-
|
234
|
-
(
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
})
|
352
|
+
pagePromises.push(
|
353
|
+
queue.addPrompt({
|
354
|
+
prompt: screen.requirements,
|
355
|
+
method: screen.method,
|
356
|
+
path: screen.path,
|
357
|
+
description: screen.requirements,
|
358
|
+
name: screen.name,
|
359
|
+
title: screen.title,
|
360
|
+
filename: screen.filename,
|
361
|
+
storage_prefix: outerConversationId + '_',
|
362
|
+
shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
|
363
|
+
})
|
291
364
|
);
|
292
365
|
}
|
293
366
|
|
294
367
|
await queue.wait();
|
368
|
+
await Promise.allSettled(pagePromises);
|
369
|
+
await Promise.allSettled(pageEventPromises);
|
295
370
|
|
296
371
|
if (userJourneysStream.isAborted()) {
|
297
372
|
return;
|
@@ -581,6 +656,7 @@ function sendError(err: Error, res: Response) {
|
|
581
656
|
res.status(400).send({ error: err.message });
|
582
657
|
}
|
583
658
|
}
|
659
|
+
|
584
660
|
function waitForStormStream(result: StormStream) {
|
585
661
|
return result.waitForDone();
|
586
662
|
}
|