@kapeta/local-cluster-service 0.67.3 → 0.67.5
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 +15 -0
- package/dist/cjs/src/storm/PageGenerator.d.ts +7 -8
- package/dist/cjs/src/storm/PageGenerator.js +49 -49
- package/dist/cjs/src/storm/page-utils.js +4 -10
- package/dist/cjs/src/storm/routes.js +19 -36
- package/dist/esm/src/storm/PageGenerator.d.ts +7 -8
- package/dist/esm/src/storm/PageGenerator.js +49 -49
- package/dist/esm/src/storm/page-utils.js +4 -10
- package/dist/esm/src/storm/routes.js +19 -36
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +63 -73
- package/src/storm/page-utils.ts +4 -10
- package/src/storm/routes.ts +20 -37
- package/src/storm/stormClient.ts +1 -1
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## [0.67.5](https://github.com/kapetacom/local-cluster-service/compare/v0.67.4...v0.67.5) (2024-09-02)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* better tracking of async events [CORE-3409] ([5e8f8cd](https://github.com/kapetacom/local-cluster-service/commit/5e8f8cdbe7e714cf0dfb5b0e7cbb8b60f837c2f9))
|
7
|
+
* dont generate individual pages when page matches path w/ params ([c3ca35c](https://github.com/kapetacom/local-cluster-service/commit/c3ca35ce56781ca90f3ca1070fee76aa3f5512ab))
|
8
|
+
|
9
|
+
## [0.67.4](https://github.com/kapetacom/local-cluster-service/compare/v0.67.3...v0.67.4) (2024-08-30)
|
10
|
+
|
11
|
+
|
12
|
+
### Bug Fixes
|
13
|
+
|
14
|
+
* Use theme.md content ([dde00ac](https://github.com/kapetacom/local-cluster-service/commit/dde00accee6226e367b0cff33b66a6e98b27b4f0))
|
15
|
+
|
1
16
|
## [0.67.3](https://github.com/kapetacom/local-cluster-service/compare/v0.67.2...v0.67.3) (2024-08-30)
|
2
17
|
|
3
18
|
|
@@ -6,7 +6,6 @@
|
|
6
6
|
import { UIPagePrompt } from './stormClient';
|
7
7
|
import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
|
8
8
|
import { EventEmitter } from 'node:events';
|
9
|
-
import { FuturePromise } from './PromiseQueue';
|
10
9
|
export interface ImagePrompt {
|
11
10
|
name: string;
|
12
11
|
description: string;
|
@@ -21,6 +20,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & {
|
|
21
20
|
};
|
22
21
|
export declare class PageQueue extends EventEmitter {
|
23
22
|
private readonly queue;
|
23
|
+
private readonly eventQueue;
|
24
24
|
private readonly systemId;
|
25
25
|
private readonly systemPrompt;
|
26
26
|
private readonly references;
|
@@ -29,12 +29,12 @@ export declare class PageQueue extends EventEmitter {
|
|
29
29
|
private uiShells;
|
30
30
|
private theme;
|
31
31
|
constructor(systemId: string, systemPrompt: string, concurrency?: number);
|
32
|
-
on(event: 'event', listener: (data: StormEvent) => void): this;
|
33
|
-
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
34
|
-
on(event: 'image', listener: (data: StormImage, source: ImagePrompt
|
32
|
+
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
33
|
+
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
34
|
+
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
35
35
|
emit(type: 'event', event: StormEvent): boolean;
|
36
36
|
emit(type: 'page', event: StormEventPage): boolean;
|
37
|
-
emit(type: 'image', event: StormImage, source: ImagePrompt
|
37
|
+
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
38
38
|
addUiShell(uiShell: UIShell): void;
|
39
39
|
setUiTheme(theme: string): void;
|
40
40
|
private hasPrompt;
|
@@ -47,6 +47,7 @@ export declare class PageQueue extends EventEmitter {
|
|
47
47
|
private addImagePrompt;
|
48
48
|
}
|
49
49
|
export declare class PageGenerator extends EventEmitter {
|
50
|
+
private readonly eventQueue;
|
50
51
|
private readonly conversationId;
|
51
52
|
readonly prompt: UIPagePrompt;
|
52
53
|
constructor(prompt: UIPagePrompt, conversationId?: string);
|
@@ -54,13 +55,11 @@ export declare class PageGenerator extends EventEmitter {
|
|
54
55
|
on(event: 'page_refs', listener: (data: {
|
55
56
|
event: StormEventPage;
|
56
57
|
references: ReferenceClassification[];
|
57
|
-
|
58
|
-
}) => void): this;
|
58
|
+
}) => Promise<void>): this;
|
59
59
|
emit(type: 'event', event: StormEvent): boolean;
|
60
60
|
emit(type: 'page_refs', event: {
|
61
61
|
event: StormEventPage;
|
62
62
|
references: ReferenceClassification[];
|
63
|
-
future: FuturePromise<void>;
|
64
63
|
}): boolean;
|
65
64
|
generate(): Promise<void>;
|
66
65
|
private resolveReferences;
|
@@ -15,6 +15,7 @@ const PromiseQueue_1 = require("./PromiseQueue");
|
|
15
15
|
const page_utils_1 = require("./page-utils");
|
16
16
|
class PageQueue extends node_events_1.EventEmitter {
|
17
17
|
queue;
|
18
|
+
eventQueue;
|
18
19
|
systemId;
|
19
20
|
systemPrompt;
|
20
21
|
references = new Map();
|
@@ -27,9 +28,12 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
27
28
|
this.systemId = systemId;
|
28
29
|
this.systemPrompt = systemPrompt;
|
29
30
|
this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
|
31
|
+
this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
|
30
32
|
}
|
31
33
|
on(event, listener) {
|
32
|
-
return super.on(event,
|
34
|
+
return super.on(event, (...args) => {
|
35
|
+
void this.eventQueue.add(async () => listener(...args));
|
36
|
+
});
|
33
37
|
}
|
34
38
|
emit(eventName, ...args) {
|
35
39
|
return super.emit(eventName, ...args);
|
@@ -56,6 +60,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
56
60
|
//console.log('Ignoring duplicate prompt', initialPrompt.path);
|
57
61
|
return Promise.resolve();
|
58
62
|
}
|
63
|
+
console.log('Generating page for', initialPrompt.method, initialPrompt.path);
|
59
64
|
const prompt = {
|
60
65
|
...initialPrompt,
|
61
66
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
@@ -75,7 +80,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
75
80
|
return promptPrefix;
|
76
81
|
}
|
77
82
|
wrapPagePrompt(pagePath, prompt) {
|
78
|
-
|
83
|
+
const promptPrefix = this.getPrefix();
|
79
84
|
let promptPostfix = '';
|
80
85
|
if (this.pages.size > 0) {
|
81
86
|
promptPostfix = `\nThe following pages are already implemented:\n`;
|
@@ -96,10 +101,10 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
96
101
|
}
|
97
102
|
async addPageGenerator(generator) {
|
98
103
|
generator.on('event', (event) => this.emit('event', event));
|
99
|
-
generator.on('page_refs', async ({ event, references
|
104
|
+
generator.on('page_refs', async ({ event, references }) => {
|
100
105
|
try {
|
101
106
|
const initialPrompts = [];
|
102
|
-
|
107
|
+
const resourcePromises = references.map(async (reference) => {
|
103
108
|
if (reference.url.startsWith('#') ||
|
104
109
|
reference.url.startsWith('javascript:') ||
|
105
110
|
reference.url.startsWith('http://') ||
|
@@ -135,8 +140,11 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
135
140
|
break;
|
136
141
|
}
|
137
142
|
});
|
138
|
-
|
139
|
-
|
143
|
+
// Wait for resources to be generated
|
144
|
+
await Promise.allSettled(resourcePromises);
|
145
|
+
this.emit('page', event);
|
146
|
+
// Emit any new pages after the current page to increase responsiveness
|
147
|
+
const newPages = initialPrompts.map((prompt) => {
|
140
148
|
if (!this.hasPrompt(prompt.path)) {
|
141
149
|
this.emit('page', {
|
142
150
|
type: 'PAGE',
|
@@ -155,21 +163,24 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
155
163
|
},
|
156
164
|
});
|
157
165
|
}
|
158
|
-
this.addPrompt(prompt);
|
166
|
+
return this.addPrompt(prompt);
|
159
167
|
});
|
160
|
-
|
168
|
+
await Promise.allSettled(newPages);
|
161
169
|
}
|
162
|
-
|
163
|
-
|
170
|
+
catch (e) {
|
171
|
+
console.error('Failed to process event', e);
|
172
|
+
throw e;
|
164
173
|
}
|
165
174
|
});
|
166
175
|
return this.queue.add(() => generator.generate());
|
167
176
|
}
|
168
177
|
cancel() {
|
169
178
|
this.queue.cancel();
|
179
|
+
this.eventQueue.cancel();
|
170
180
|
}
|
171
|
-
wait() {
|
172
|
-
|
181
|
+
async wait() {
|
182
|
+
await this.eventQueue.wait();
|
183
|
+
await this.queue.wait();
|
173
184
|
}
|
174
185
|
async addImagePrompt(prompt) {
|
175
186
|
if (this.images.has(prompt.url)) {
|
@@ -180,65 +191,54 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
180
191
|
const prefix = this.getPrefix();
|
181
192
|
const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
|
182
193
|
//console.log('Adding image prompt', prompt);
|
183
|
-
|
184
|
-
result.on('data', async (event) => {
|
194
|
+
result.on('data', (event) => {
|
185
195
|
if (event.type === 'IMAGE') {
|
186
|
-
|
187
|
-
futures.push(future);
|
188
|
-
this.emit('image', event, prompt, future);
|
189
|
-
setTimeout(() => {
|
190
|
-
if (!future.isResolved()) {
|
191
|
-
console.log('Image prompt timed out', prompt);
|
192
|
-
future.reject(new Error('Image prompt timed out'));
|
193
|
-
}
|
194
|
-
}, 30000);
|
196
|
+
this.emit('image', event, prompt);
|
195
197
|
}
|
196
198
|
});
|
197
199
|
await result.waitForDone();
|
198
|
-
await Promise.allSettled(futures.map((f) => f.promise));
|
199
200
|
}
|
200
201
|
}
|
201
202
|
exports.PageQueue = PageQueue;
|
202
203
|
class PageGenerator extends node_events_1.EventEmitter {
|
204
|
+
eventQueue;
|
203
205
|
conversationId;
|
204
206
|
prompt;
|
205
207
|
constructor(prompt, conversationId = node_uuid_1.default.v4()) {
|
206
208
|
super();
|
207
209
|
this.conversationId = conversationId;
|
208
210
|
this.prompt = prompt;
|
211
|
+
this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
|
209
212
|
}
|
210
213
|
on(event, listener) {
|
211
|
-
return super.on(event,
|
214
|
+
return super.on(event, (...args) => {
|
215
|
+
void this.eventQueue.add(async () => listener(...args));
|
216
|
+
});
|
212
217
|
}
|
213
218
|
emit(eventName, ...args) {
|
214
219
|
return super.emit(eventName, ...args);
|
215
220
|
}
|
216
221
|
async generate() {
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
})());
|
234
|
-
return;
|
235
|
-
}
|
236
|
-
this.emit('event', event);
|
237
|
-
});
|
238
|
-
await screenStream.waitForDone();
|
239
|
-
await Promise.all(promises);
|
240
|
-
resolve();
|
222
|
+
const promises = [];
|
223
|
+
const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
|
224
|
+
screenStream.on('data', (event) => {
|
225
|
+
if (event.type === 'PAGE') {
|
226
|
+
event.payload.conversationId = this.conversationId;
|
227
|
+
promises.push((async () => {
|
228
|
+
const references = await this.resolveReferences(event.payload.content);
|
229
|
+
//console.log('Resolved references for page', references, event.payload);
|
230
|
+
this.emit('page_refs', {
|
231
|
+
event,
|
232
|
+
references,
|
233
|
+
});
|
234
|
+
})());
|
235
|
+
return;
|
236
|
+
}
|
237
|
+
this.emit('event', event);
|
241
238
|
});
|
239
|
+
await screenStream.waitForDone();
|
240
|
+
await Promise.all(promises);
|
241
|
+
await this.eventQueue.wait();
|
242
242
|
}
|
243
243
|
async resolveReferences(content) {
|
244
244
|
const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
|
@@ -12,7 +12,7 @@ function normalizePath(path) {
|
|
12
12
|
return path
|
13
13
|
.replace(/\?.*$/gi, '')
|
14
14
|
.replace(/:[a-z][a-z_]*\b/gi, '*')
|
15
|
-
.replace(/\{[a-z]+}/gi, '*');
|
15
|
+
.replace(/\{[a-z-.]+}/gi, '*');
|
16
16
|
}
|
17
17
|
async function writePageToDisk(systemId, event) {
|
18
18
|
const baseDir = getSystemBaseDir(systemId);
|
@@ -53,13 +53,8 @@ async function writeImageToDisk(systemId, event, prompt) {
|
|
53
53
|
}
|
54
54
|
exports.writeImageToDisk = writeImageToDisk;
|
55
55
|
function hasPageOnDisk(systemId, method, path) {
|
56
|
-
|
57
|
-
|
58
|
-
}
|
59
|
-
const baseDir = getSystemBaseDir(systemId);
|
60
|
-
const filePath = getFilePath(method);
|
61
|
-
const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
|
62
|
-
return fs_extra_1.default.existsSync(fullPath);
|
56
|
+
const fullPath = resolveReadPath(systemId, method, path);
|
57
|
+
return !!fullPath && fs_extra_1.default.existsSync(fullPath);
|
63
58
|
}
|
64
59
|
exports.hasPageOnDisk = hasPageOnDisk;
|
65
60
|
function getSystemBaseDir(systemId) {
|
@@ -90,7 +85,7 @@ function resolveReadPath(systemId, path, method) {
|
|
90
85
|
}
|
91
86
|
const parts = path.split(/\*/g);
|
92
87
|
let currentPath = '';
|
93
|
-
for (let part
|
88
|
+
for (let part of parts) {
|
94
89
|
const thisPath = path_1.default.join(currentPath, part);
|
95
90
|
const starPath = path_1.default.join(currentPath, '*');
|
96
91
|
const thisPathDir = path_1.default.join(baseDir, thisPath);
|
@@ -103,7 +98,6 @@ function resolveReadPath(systemId, path, method) {
|
|
103
98
|
currentPath = starPath;
|
104
99
|
continue;
|
105
100
|
}
|
106
|
-
console.log('Path not found', thisPathDir, starPathDir);
|
107
101
|
// Path not found
|
108
102
|
return null;
|
109
103
|
}
|
@@ -74,24 +74,17 @@ router.post('/ui/screen', async (req, res) => {
|
|
74
74
|
queue.cancel();
|
75
75
|
});
|
76
76
|
const promises = [];
|
77
|
-
queue.on('page', (data) =>
|
78
|
-
|
79
|
-
promises.push(sendPageEvent(systemId, data, res));
|
80
|
-
}
|
81
|
-
});
|
82
|
-
queue.on('image', async (screenData, prompt, future) => {
|
77
|
+
queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
|
78
|
+
queue.on('image', async (screenData, prompt) => {
|
83
79
|
if (!systemId) {
|
84
80
|
return;
|
85
81
|
}
|
86
82
|
try {
|
87
|
-
|
88
|
-
promises.push(promise);
|
89
|
-
await promise;
|
90
|
-
future.resolve();
|
83
|
+
await handleImageEvent(systemId, screenData, prompt);
|
91
84
|
}
|
92
85
|
catch (e) {
|
93
86
|
console.error('Failed to handle image event', e);
|
94
|
-
|
87
|
+
throw e;
|
95
88
|
}
|
96
89
|
});
|
97
90
|
await queue.addPrompt(aiRequest, conversationId, true);
|
@@ -186,19 +179,14 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
186
179
|
onRequestAborted(req, res, () => {
|
187
180
|
pageQueue.cancel();
|
188
181
|
});
|
189
|
-
pageQueue.on('page', (screenData) =>
|
190
|
-
|
191
|
-
});
|
192
|
-
pageQueue.on('image', async (screenData, prompt, future) => {
|
182
|
+
pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
|
183
|
+
pageQueue.on('image', async (screenData, prompt) => {
|
193
184
|
try {
|
194
|
-
|
195
|
-
pageEventPromises.push(promise);
|
196
|
-
await promise;
|
197
|
-
future.resolve();
|
185
|
+
await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
|
198
186
|
}
|
199
187
|
catch (e) {
|
200
188
|
console.error('Failed to handle image event', e);
|
201
|
-
|
189
|
+
throw e;
|
202
190
|
}
|
203
191
|
});
|
204
192
|
pageQueue.on('event', (screenData) => {
|
@@ -303,7 +291,7 @@ router.post('/:handle/ui', async (req, res) => {
|
|
303
291
|
await waitForStormStream(userJourneysStream);
|
304
292
|
// Get the UI shells
|
305
293
|
const shellsStream = await stormClient_1.stormClient.createUIShells({
|
306
|
-
theme: theme
|
294
|
+
theme: theme || undefined,
|
307
295
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
308
296
|
name: screen.name,
|
309
297
|
title: screen.title,
|
@@ -346,31 +334,25 @@ router.post('/:handle/ui', async (req, res) => {
|
|
346
334
|
},
|
347
335
|
created: Date.now(),
|
348
336
|
});
|
349
|
-
const pagePromises = [];
|
350
337
|
onRequestAborted(req, res, () => {
|
351
338
|
queue.cancel();
|
352
339
|
});
|
353
|
-
|
354
|
-
queue.on('
|
355
|
-
pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
|
356
|
-
});
|
357
|
-
queue.on('image', async (screenData, prompt, future) => {
|
340
|
+
queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
|
341
|
+
queue.on('image', async (screenData, prompt) => {
|
358
342
|
try {
|
359
|
-
|
360
|
-
pageEventPromises.push(promise);
|
361
|
-
await promise;
|
362
|
-
future.resolve();
|
343
|
+
await handleImageEvent(outerConversationId, screenData, prompt);
|
363
344
|
}
|
364
345
|
catch (e) {
|
365
346
|
console.error('Failed to handle image event', e);
|
366
|
-
|
347
|
+
throw e;
|
367
348
|
}
|
368
349
|
});
|
369
350
|
queue.on('event', (screenData) => {
|
370
351
|
sendEvent(res, screenData);
|
371
352
|
});
|
372
353
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
373
|
-
|
354
|
+
queue
|
355
|
+
.addPrompt({
|
374
356
|
prompt: screen.requirements,
|
375
357
|
method: screen.method,
|
376
358
|
path: screen.path,
|
@@ -380,14 +362,15 @@ router.post('/:handle/ui', async (req, res) => {
|
|
380
362
|
filename: screen.filename,
|
381
363
|
storage_prefix: outerConversationId + '_',
|
382
364
|
theme,
|
383
|
-
})
|
365
|
+
})
|
366
|
+
.catch((e) => {
|
367
|
+
console.error('Failed to generate page for screen %s', screen.name, e);
|
368
|
+
});
|
384
369
|
}
|
385
370
|
if (userJourneysStream.isAborted()) {
|
386
371
|
return;
|
387
372
|
}
|
388
373
|
await queue.wait();
|
389
|
-
await Promise.allSettled(pagePromises);
|
390
|
-
await Promise.allSettled(pageEventPromises);
|
391
374
|
sendDone(res);
|
392
375
|
}
|
393
376
|
catch (err) {
|
@@ -6,7 +6,6 @@
|
|
6
6
|
import { UIPagePrompt } from './stormClient';
|
7
7
|
import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
|
8
8
|
import { EventEmitter } from 'node:events';
|
9
|
-
import { FuturePromise } from './PromiseQueue';
|
10
9
|
export interface ImagePrompt {
|
11
10
|
name: string;
|
12
11
|
description: string;
|
@@ -21,6 +20,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & {
|
|
21
20
|
};
|
22
21
|
export declare class PageQueue extends EventEmitter {
|
23
22
|
private readonly queue;
|
23
|
+
private readonly eventQueue;
|
24
24
|
private readonly systemId;
|
25
25
|
private readonly systemPrompt;
|
26
26
|
private readonly references;
|
@@ -29,12 +29,12 @@ export declare class PageQueue extends EventEmitter {
|
|
29
29
|
private uiShells;
|
30
30
|
private theme;
|
31
31
|
constructor(systemId: string, systemPrompt: string, concurrency?: number);
|
32
|
-
on(event: 'event', listener: (data: StormEvent) => void): this;
|
33
|
-
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
34
|
-
on(event: 'image', listener: (data: StormImage, source: ImagePrompt
|
32
|
+
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
33
|
+
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
34
|
+
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
35
35
|
emit(type: 'event', event: StormEvent): boolean;
|
36
36
|
emit(type: 'page', event: StormEventPage): boolean;
|
37
|
-
emit(type: 'image', event: StormImage, source: ImagePrompt
|
37
|
+
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
38
38
|
addUiShell(uiShell: UIShell): void;
|
39
39
|
setUiTheme(theme: string): void;
|
40
40
|
private hasPrompt;
|
@@ -47,6 +47,7 @@ export declare class PageQueue extends EventEmitter {
|
|
47
47
|
private addImagePrompt;
|
48
48
|
}
|
49
49
|
export declare class PageGenerator extends EventEmitter {
|
50
|
+
private readonly eventQueue;
|
50
51
|
private readonly conversationId;
|
51
52
|
readonly prompt: UIPagePrompt;
|
52
53
|
constructor(prompt: UIPagePrompt, conversationId?: string);
|
@@ -54,13 +55,11 @@ export declare class PageGenerator extends EventEmitter {
|
|
54
55
|
on(event: 'page_refs', listener: (data: {
|
55
56
|
event: StormEventPage;
|
56
57
|
references: ReferenceClassification[];
|
57
|
-
|
58
|
-
}) => void): this;
|
58
|
+
}) => Promise<void>): this;
|
59
59
|
emit(type: 'event', event: StormEvent): boolean;
|
60
60
|
emit(type: 'page_refs', event: {
|
61
61
|
event: StormEventPage;
|
62
62
|
references: ReferenceClassification[];
|
63
|
-
future: FuturePromise<void>;
|
64
63
|
}): boolean;
|
65
64
|
generate(): Promise<void>;
|
66
65
|
private resolveReferences;
|
@@ -15,6 +15,7 @@ const PromiseQueue_1 = require("./PromiseQueue");
|
|
15
15
|
const page_utils_1 = require("./page-utils");
|
16
16
|
class PageQueue extends node_events_1.EventEmitter {
|
17
17
|
queue;
|
18
|
+
eventQueue;
|
18
19
|
systemId;
|
19
20
|
systemPrompt;
|
20
21
|
references = new Map();
|
@@ -27,9 +28,12 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
27
28
|
this.systemId = systemId;
|
28
29
|
this.systemPrompt = systemPrompt;
|
29
30
|
this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
|
31
|
+
this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
|
30
32
|
}
|
31
33
|
on(event, listener) {
|
32
|
-
return super.on(event,
|
34
|
+
return super.on(event, (...args) => {
|
35
|
+
void this.eventQueue.add(async () => listener(...args));
|
36
|
+
});
|
33
37
|
}
|
34
38
|
emit(eventName, ...args) {
|
35
39
|
return super.emit(eventName, ...args);
|
@@ -56,6 +60,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
56
60
|
//console.log('Ignoring duplicate prompt', initialPrompt.path);
|
57
61
|
return Promise.resolve();
|
58
62
|
}
|
63
|
+
console.log('Generating page for', initialPrompt.method, initialPrompt.path);
|
59
64
|
const prompt = {
|
60
65
|
...initialPrompt,
|
61
66
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
@@ -75,7 +80,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
75
80
|
return promptPrefix;
|
76
81
|
}
|
77
82
|
wrapPagePrompt(pagePath, prompt) {
|
78
|
-
|
83
|
+
const promptPrefix = this.getPrefix();
|
79
84
|
let promptPostfix = '';
|
80
85
|
if (this.pages.size > 0) {
|
81
86
|
promptPostfix = `\nThe following pages are already implemented:\n`;
|
@@ -96,10 +101,10 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
96
101
|
}
|
97
102
|
async addPageGenerator(generator) {
|
98
103
|
generator.on('event', (event) => this.emit('event', event));
|
99
|
-
generator.on('page_refs', async ({ event, references
|
104
|
+
generator.on('page_refs', async ({ event, references }) => {
|
100
105
|
try {
|
101
106
|
const initialPrompts = [];
|
102
|
-
|
107
|
+
const resourcePromises = references.map(async (reference) => {
|
103
108
|
if (reference.url.startsWith('#') ||
|
104
109
|
reference.url.startsWith('javascript:') ||
|
105
110
|
reference.url.startsWith('http://') ||
|
@@ -135,8 +140,11 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
135
140
|
break;
|
136
141
|
}
|
137
142
|
});
|
138
|
-
|
139
|
-
|
143
|
+
// Wait for resources to be generated
|
144
|
+
await Promise.allSettled(resourcePromises);
|
145
|
+
this.emit('page', event);
|
146
|
+
// Emit any new pages after the current page to increase responsiveness
|
147
|
+
const newPages = initialPrompts.map((prompt) => {
|
140
148
|
if (!this.hasPrompt(prompt.path)) {
|
141
149
|
this.emit('page', {
|
142
150
|
type: 'PAGE',
|
@@ -155,21 +163,24 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
155
163
|
},
|
156
164
|
});
|
157
165
|
}
|
158
|
-
this.addPrompt(prompt);
|
166
|
+
return this.addPrompt(prompt);
|
159
167
|
});
|
160
|
-
|
168
|
+
await Promise.allSettled(newPages);
|
161
169
|
}
|
162
|
-
|
163
|
-
|
170
|
+
catch (e) {
|
171
|
+
console.error('Failed to process event', e);
|
172
|
+
throw e;
|
164
173
|
}
|
165
174
|
});
|
166
175
|
return this.queue.add(() => generator.generate());
|
167
176
|
}
|
168
177
|
cancel() {
|
169
178
|
this.queue.cancel();
|
179
|
+
this.eventQueue.cancel();
|
170
180
|
}
|
171
|
-
wait() {
|
172
|
-
|
181
|
+
async wait() {
|
182
|
+
await this.eventQueue.wait();
|
183
|
+
await this.queue.wait();
|
173
184
|
}
|
174
185
|
async addImagePrompt(prompt) {
|
175
186
|
if (this.images.has(prompt.url)) {
|
@@ -180,65 +191,54 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
180
191
|
const prefix = this.getPrefix();
|
181
192
|
const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
|
182
193
|
//console.log('Adding image prompt', prompt);
|
183
|
-
|
184
|
-
result.on('data', async (event) => {
|
194
|
+
result.on('data', (event) => {
|
185
195
|
if (event.type === 'IMAGE') {
|
186
|
-
|
187
|
-
futures.push(future);
|
188
|
-
this.emit('image', event, prompt, future);
|
189
|
-
setTimeout(() => {
|
190
|
-
if (!future.isResolved()) {
|
191
|
-
console.log('Image prompt timed out', prompt);
|
192
|
-
future.reject(new Error('Image prompt timed out'));
|
193
|
-
}
|
194
|
-
}, 30000);
|
196
|
+
this.emit('image', event, prompt);
|
195
197
|
}
|
196
198
|
});
|
197
199
|
await result.waitForDone();
|
198
|
-
await Promise.allSettled(futures.map((f) => f.promise));
|
199
200
|
}
|
200
201
|
}
|
201
202
|
exports.PageQueue = PageQueue;
|
202
203
|
class PageGenerator extends node_events_1.EventEmitter {
|
204
|
+
eventQueue;
|
203
205
|
conversationId;
|
204
206
|
prompt;
|
205
207
|
constructor(prompt, conversationId = node_uuid_1.default.v4()) {
|
206
208
|
super();
|
207
209
|
this.conversationId = conversationId;
|
208
210
|
this.prompt = prompt;
|
211
|
+
this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
|
209
212
|
}
|
210
213
|
on(event, listener) {
|
211
|
-
return super.on(event,
|
214
|
+
return super.on(event, (...args) => {
|
215
|
+
void this.eventQueue.add(async () => listener(...args));
|
216
|
+
});
|
212
217
|
}
|
213
218
|
emit(eventName, ...args) {
|
214
219
|
return super.emit(eventName, ...args);
|
215
220
|
}
|
216
221
|
async generate() {
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
})());
|
234
|
-
return;
|
235
|
-
}
|
236
|
-
this.emit('event', event);
|
237
|
-
});
|
238
|
-
await screenStream.waitForDone();
|
239
|
-
await Promise.all(promises);
|
240
|
-
resolve();
|
222
|
+
const promises = [];
|
223
|
+
const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
|
224
|
+
screenStream.on('data', (event) => {
|
225
|
+
if (event.type === 'PAGE') {
|
226
|
+
event.payload.conversationId = this.conversationId;
|
227
|
+
promises.push((async () => {
|
228
|
+
const references = await this.resolveReferences(event.payload.content);
|
229
|
+
//console.log('Resolved references for page', references, event.payload);
|
230
|
+
this.emit('page_refs', {
|
231
|
+
event,
|
232
|
+
references,
|
233
|
+
});
|
234
|
+
})());
|
235
|
+
return;
|
236
|
+
}
|
237
|
+
this.emit('event', event);
|
241
238
|
});
|
239
|
+
await screenStream.waitForDone();
|
240
|
+
await Promise.all(promises);
|
241
|
+
await this.eventQueue.wait();
|
242
242
|
}
|
243
243
|
async resolveReferences(content) {
|
244
244
|
const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
|
@@ -12,7 +12,7 @@ function normalizePath(path) {
|
|
12
12
|
return path
|
13
13
|
.replace(/\?.*$/gi, '')
|
14
14
|
.replace(/:[a-z][a-z_]*\b/gi, '*')
|
15
|
-
.replace(/\{[a-z]+}/gi, '*');
|
15
|
+
.replace(/\{[a-z-.]+}/gi, '*');
|
16
16
|
}
|
17
17
|
async function writePageToDisk(systemId, event) {
|
18
18
|
const baseDir = getSystemBaseDir(systemId);
|
@@ -53,13 +53,8 @@ async function writeImageToDisk(systemId, event, prompt) {
|
|
53
53
|
}
|
54
54
|
exports.writeImageToDisk = writeImageToDisk;
|
55
55
|
function hasPageOnDisk(systemId, method, path) {
|
56
|
-
|
57
|
-
|
58
|
-
}
|
59
|
-
const baseDir = getSystemBaseDir(systemId);
|
60
|
-
const filePath = getFilePath(method);
|
61
|
-
const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
|
62
|
-
return fs_extra_1.default.existsSync(fullPath);
|
56
|
+
const fullPath = resolveReadPath(systemId, method, path);
|
57
|
+
return !!fullPath && fs_extra_1.default.existsSync(fullPath);
|
63
58
|
}
|
64
59
|
exports.hasPageOnDisk = hasPageOnDisk;
|
65
60
|
function getSystemBaseDir(systemId) {
|
@@ -90,7 +85,7 @@ function resolveReadPath(systemId, path, method) {
|
|
90
85
|
}
|
91
86
|
const parts = path.split(/\*/g);
|
92
87
|
let currentPath = '';
|
93
|
-
for (let part
|
88
|
+
for (let part of parts) {
|
94
89
|
const thisPath = path_1.default.join(currentPath, part);
|
95
90
|
const starPath = path_1.default.join(currentPath, '*');
|
96
91
|
const thisPathDir = path_1.default.join(baseDir, thisPath);
|
@@ -103,7 +98,6 @@ function resolveReadPath(systemId, path, method) {
|
|
103
98
|
currentPath = starPath;
|
104
99
|
continue;
|
105
100
|
}
|
106
|
-
console.log('Path not found', thisPathDir, starPathDir);
|
107
101
|
// Path not found
|
108
102
|
return null;
|
109
103
|
}
|
@@ -74,24 +74,17 @@ router.post('/ui/screen', async (req, res) => {
|
|
74
74
|
queue.cancel();
|
75
75
|
});
|
76
76
|
const promises = [];
|
77
|
-
queue.on('page', (data) =>
|
78
|
-
|
79
|
-
promises.push(sendPageEvent(systemId, data, res));
|
80
|
-
}
|
81
|
-
});
|
82
|
-
queue.on('image', async (screenData, prompt, future) => {
|
77
|
+
queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
|
78
|
+
queue.on('image', async (screenData, prompt) => {
|
83
79
|
if (!systemId) {
|
84
80
|
return;
|
85
81
|
}
|
86
82
|
try {
|
87
|
-
|
88
|
-
promises.push(promise);
|
89
|
-
await promise;
|
90
|
-
future.resolve();
|
83
|
+
await handleImageEvent(systemId, screenData, prompt);
|
91
84
|
}
|
92
85
|
catch (e) {
|
93
86
|
console.error('Failed to handle image event', e);
|
94
|
-
|
87
|
+
throw e;
|
95
88
|
}
|
96
89
|
});
|
97
90
|
await queue.addPrompt(aiRequest, conversationId, true);
|
@@ -186,19 +179,14 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
186
179
|
onRequestAborted(req, res, () => {
|
187
180
|
pageQueue.cancel();
|
188
181
|
});
|
189
|
-
pageQueue.on('page', (screenData) =>
|
190
|
-
|
191
|
-
});
|
192
|
-
pageQueue.on('image', async (screenData, prompt, future) => {
|
182
|
+
pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
|
183
|
+
pageQueue.on('image', async (screenData, prompt) => {
|
193
184
|
try {
|
194
|
-
|
195
|
-
pageEventPromises.push(promise);
|
196
|
-
await promise;
|
197
|
-
future.resolve();
|
185
|
+
await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
|
198
186
|
}
|
199
187
|
catch (e) {
|
200
188
|
console.error('Failed to handle image event', e);
|
201
|
-
|
189
|
+
throw e;
|
202
190
|
}
|
203
191
|
});
|
204
192
|
pageQueue.on('event', (screenData) => {
|
@@ -303,7 +291,7 @@ router.post('/:handle/ui', async (req, res) => {
|
|
303
291
|
await waitForStormStream(userJourneysStream);
|
304
292
|
// Get the UI shells
|
305
293
|
const shellsStream = await stormClient_1.stormClient.createUIShells({
|
306
|
-
theme: theme
|
294
|
+
theme: theme || undefined,
|
307
295
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
308
296
|
name: screen.name,
|
309
297
|
title: screen.title,
|
@@ -346,31 +334,25 @@ router.post('/:handle/ui', async (req, res) => {
|
|
346
334
|
},
|
347
335
|
created: Date.now(),
|
348
336
|
});
|
349
|
-
const pagePromises = [];
|
350
337
|
onRequestAborted(req, res, () => {
|
351
338
|
queue.cancel();
|
352
339
|
});
|
353
|
-
|
354
|
-
queue.on('
|
355
|
-
pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
|
356
|
-
});
|
357
|
-
queue.on('image', async (screenData, prompt, future) => {
|
340
|
+
queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
|
341
|
+
queue.on('image', async (screenData, prompt) => {
|
358
342
|
try {
|
359
|
-
|
360
|
-
pageEventPromises.push(promise);
|
361
|
-
await promise;
|
362
|
-
future.resolve();
|
343
|
+
await handleImageEvent(outerConversationId, screenData, prompt);
|
363
344
|
}
|
364
345
|
catch (e) {
|
365
346
|
console.error('Failed to handle image event', e);
|
366
|
-
|
347
|
+
throw e;
|
367
348
|
}
|
368
349
|
});
|
369
350
|
queue.on('event', (screenData) => {
|
370
351
|
sendEvent(res, screenData);
|
371
352
|
});
|
372
353
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
373
|
-
|
354
|
+
queue
|
355
|
+
.addPrompt({
|
374
356
|
prompt: screen.requirements,
|
375
357
|
method: screen.method,
|
376
358
|
path: screen.path,
|
@@ -380,14 +362,15 @@ router.post('/:handle/ui', async (req, res) => {
|
|
380
362
|
filename: screen.filename,
|
381
363
|
storage_prefix: outerConversationId + '_',
|
382
364
|
theme,
|
383
|
-
})
|
365
|
+
})
|
366
|
+
.catch((e) => {
|
367
|
+
console.error('Failed to generate page for screen %s', screen.name, e);
|
368
|
+
});
|
384
369
|
}
|
385
370
|
if (userJourneysStream.isAborted()) {
|
386
371
|
return;
|
387
372
|
}
|
388
373
|
await queue.wait();
|
389
|
-
await Promise.allSettled(pagePromises);
|
390
|
-
await Promise.allSettled(pageEventPromises);
|
391
374
|
sendDone(res);
|
392
375
|
}
|
393
376
|
catch (err) {
|
package/package.json
CHANGED
@@ -7,7 +7,7 @@ import uuid from 'node-uuid';
|
|
7
7
|
import { stormClient, UIPagePrompt } from './stormClient';
|
8
8
|
import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
|
9
9
|
import { EventEmitter } from 'node:events';
|
10
|
-
import {
|
10
|
+
import { PromiseQueue } from './PromiseQueue';
|
11
11
|
import { hasPageOnDisk } from './page-utils';
|
12
12
|
|
13
13
|
export interface ImagePrompt {
|
@@ -24,6 +24,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & { shellType?: 'public' |
|
|
24
24
|
|
25
25
|
export class PageQueue extends EventEmitter {
|
26
26
|
private readonly queue: PromiseQueue;
|
27
|
+
private readonly eventQueue: PromiseQueue;
|
27
28
|
private readonly systemId: string;
|
28
29
|
private readonly systemPrompt: string;
|
29
30
|
private readonly references: Map<string, PageGenerator> = new Map();
|
@@ -37,19 +38,22 @@ export class PageQueue extends EventEmitter {
|
|
37
38
|
this.systemId = systemId;
|
38
39
|
this.systemPrompt = systemPrompt;
|
39
40
|
this.queue = new PromiseQueue(concurrency);
|
41
|
+
this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
|
40
42
|
}
|
41
43
|
|
42
|
-
on(event: 'event', listener: (data: StormEvent) => void): this;
|
43
|
-
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
44
|
-
on(event: 'image', listener: (data: StormImage, source: ImagePrompt
|
44
|
+
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
45
|
+
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
46
|
+
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
45
47
|
|
46
|
-
on(event: string, listener: (...args: any[]) => void): this {
|
47
|
-
return super.on(event,
|
48
|
+
on(event: string, listener: (...args: any[]) => void | Promise<void>): this {
|
49
|
+
return super.on(event, (...args) => {
|
50
|
+
void this.eventQueue.add(async () => listener(...args));
|
51
|
+
});
|
48
52
|
}
|
49
53
|
|
50
54
|
emit(type: 'event', event: StormEvent): boolean;
|
51
55
|
emit(type: 'page', event: StormEventPage): boolean;
|
52
|
-
emit(type: 'image', event: StormImage, source: ImagePrompt
|
56
|
+
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
53
57
|
emit(eventName: string | symbol, ...args: any[]): boolean {
|
54
58
|
return super.emit(eventName, ...args);
|
55
59
|
}
|
@@ -81,6 +85,7 @@ export class PageQueue extends EventEmitter {
|
|
81
85
|
//console.log('Ignoring duplicate prompt', initialPrompt.path);
|
82
86
|
return Promise.resolve();
|
83
87
|
}
|
88
|
+
console.log('Generating page for', initialPrompt.method, initialPrompt.path);
|
84
89
|
|
85
90
|
const prompt: UIPagePrompt = {
|
86
91
|
...initialPrompt,
|
@@ -104,7 +109,7 @@ export class PageQueue extends EventEmitter {
|
|
104
109
|
}
|
105
110
|
|
106
111
|
private wrapPagePrompt(pagePath: string, prompt: string): string {
|
107
|
-
|
112
|
+
const promptPrefix = this.getPrefix();
|
108
113
|
let promptPostfix = '';
|
109
114
|
|
110
115
|
if (this.pages.size > 0) {
|
@@ -129,10 +134,10 @@ export class PageQueue extends EventEmitter {
|
|
129
134
|
|
130
135
|
private async addPageGenerator(generator: PageGenerator) {
|
131
136
|
generator.on('event', (event: StormEvent) => this.emit('event', event));
|
132
|
-
generator.on('page_refs', async ({ event, references
|
137
|
+
generator.on('page_refs', async ({ event, references }) => {
|
133
138
|
try {
|
134
139
|
const initialPrompts: InitialPrompt[] = [];
|
135
|
-
|
140
|
+
const resourcePromises = references.map(async (reference) => {
|
136
141
|
if (
|
137
142
|
reference.url.startsWith('#') ||
|
138
143
|
reference.url.startsWith('javascript:') ||
|
@@ -174,8 +179,12 @@ export class PageQueue extends EventEmitter {
|
|
174
179
|
}
|
175
180
|
});
|
176
181
|
|
177
|
-
|
178
|
-
|
182
|
+
// Wait for resources to be generated
|
183
|
+
await Promise.allSettled(resourcePromises);
|
184
|
+
this.emit('page', event);
|
185
|
+
|
186
|
+
// Emit any new pages after the current page to increase responsiveness
|
187
|
+
const newPages = initialPrompts.map((prompt) => {
|
179
188
|
if (!this.hasPrompt(prompt.path)) {
|
180
189
|
this.emit('page', {
|
181
190
|
type: 'PAGE',
|
@@ -194,12 +203,12 @@ export class PageQueue extends EventEmitter {
|
|
194
203
|
},
|
195
204
|
});
|
196
205
|
}
|
197
|
-
this.addPrompt(prompt);
|
206
|
+
return this.addPrompt(prompt);
|
198
207
|
});
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
208
|
+
await Promise.allSettled(newPages);
|
209
|
+
} catch (e) {
|
210
|
+
console.error('Failed to process event', e);
|
211
|
+
throw e;
|
203
212
|
}
|
204
213
|
});
|
205
214
|
return this.queue.add(() => generator.generate());
|
@@ -207,10 +216,12 @@ export class PageQueue extends EventEmitter {
|
|
207
216
|
|
208
217
|
public cancel() {
|
209
218
|
this.queue.cancel();
|
219
|
+
this.eventQueue.cancel();
|
210
220
|
}
|
211
221
|
|
212
|
-
public wait() {
|
213
|
-
|
222
|
+
public async wait() {
|
223
|
+
await this.eventQueue.wait();
|
224
|
+
await this.queue.wait();
|
214
225
|
}
|
215
226
|
|
216
227
|
private async addImagePrompt(prompt: ImagePrompt) {
|
@@ -225,29 +236,18 @@ export class PageQueue extends EventEmitter {
|
|
225
236
|
);
|
226
237
|
|
227
238
|
//console.log('Adding image prompt', prompt);
|
228
|
-
|
229
|
-
const futures: FuturePromise<void>[] = [];
|
230
|
-
|
231
|
-
result.on('data', async (event: StormEvent) => {
|
239
|
+
result.on('data', (event: StormEvent) => {
|
232
240
|
if (event.type === 'IMAGE') {
|
233
|
-
|
234
|
-
futures.push(future);
|
235
|
-
this.emit('image', event, prompt, future);
|
236
|
-
setTimeout(() => {
|
237
|
-
if (!future.isResolved()) {
|
238
|
-
console.log('Image prompt timed out', prompt);
|
239
|
-
future.reject(new Error('Image prompt timed out'));
|
240
|
-
}
|
241
|
-
}, 30000);
|
241
|
+
this.emit('image', event, prompt);
|
242
242
|
}
|
243
243
|
});
|
244
244
|
|
245
245
|
await result.waitForDone();
|
246
|
-
await Promise.allSettled(futures.map((f) => f.promise));
|
247
246
|
}
|
248
247
|
}
|
249
248
|
|
250
249
|
export class PageGenerator extends EventEmitter {
|
250
|
+
private readonly eventQueue: PromiseQueue;
|
251
251
|
private readonly conversationId: string;
|
252
252
|
public readonly prompt: UIPagePrompt;
|
253
253
|
|
@@ -255,64 +255,54 @@ export class PageGenerator extends EventEmitter {
|
|
255
255
|
super();
|
256
256
|
this.conversationId = conversationId;
|
257
257
|
this.prompt = prompt;
|
258
|
+
this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
|
258
259
|
}
|
259
260
|
|
260
261
|
on(event: 'event', listener: (data: StormEvent) => void): this;
|
261
262
|
on(
|
262
263
|
event: 'page_refs',
|
263
|
-
listener: (data: {
|
264
|
-
event: StormEventPage;
|
265
|
-
references: ReferenceClassification[];
|
266
|
-
future: FuturePromise<void>;
|
267
|
-
}) => void
|
264
|
+
listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => Promise<void>
|
268
265
|
): this;
|
269
266
|
|
270
267
|
on(event: string, listener: (...args: any[]) => void): this {
|
271
|
-
return super.on(event,
|
268
|
+
return super.on(event, (...args) => {
|
269
|
+
void this.eventQueue.add(async () => listener(...args));
|
270
|
+
});
|
272
271
|
}
|
273
272
|
|
274
273
|
emit(type: 'event', event: StormEvent): boolean;
|
275
|
-
emit(
|
276
|
-
type: 'page_refs',
|
277
|
-
event: { event: StormEventPage; references: ReferenceClassification[]; future: FuturePromise<void> }
|
278
|
-
): boolean;
|
274
|
+
emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
|
279
275
|
emit(eventName: string | symbol, ...args: any[]): boolean {
|
280
276
|
return super.emit(eventName, ...args);
|
281
277
|
}
|
282
278
|
|
283
279
|
public async generate() {
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
await future.promise;
|
304
|
-
})()
|
305
|
-
);
|
306
|
-
return;
|
307
|
-
}
|
308
|
-
|
309
|
-
this.emit('event', event);
|
310
|
-
});
|
280
|
+
const promises: Promise<void>[] = [];
|
281
|
+
const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
|
282
|
+
|
283
|
+
screenStream.on('data', (event: StormEvent) => {
|
284
|
+
if (event.type === 'PAGE') {
|
285
|
+
event.payload.conversationId = this.conversationId;
|
286
|
+
|
287
|
+
promises.push(
|
288
|
+
(async () => {
|
289
|
+
const references = await this.resolveReferences(event.payload.content);
|
290
|
+
//console.log('Resolved references for page', references, event.payload);
|
291
|
+
this.emit('page_refs', {
|
292
|
+
event,
|
293
|
+
references,
|
294
|
+
});
|
295
|
+
})()
|
296
|
+
);
|
297
|
+
return;
|
298
|
+
}
|
311
299
|
|
312
|
-
|
313
|
-
await Promise.all(promises);
|
314
|
-
resolve();
|
300
|
+
this.emit('event', event);
|
315
301
|
});
|
302
|
+
|
303
|
+
await screenStream.waitForDone();
|
304
|
+
await Promise.all(promises);
|
305
|
+
await this.eventQueue.wait();
|
316
306
|
}
|
317
307
|
|
318
308
|
private async resolveReferences(content: string) {
|
package/src/storm/page-utils.ts
CHANGED
@@ -19,7 +19,7 @@ function normalizePath(path: string) {
|
|
19
19
|
return path
|
20
20
|
.replace(/\?.*$/gi, '')
|
21
21
|
.replace(/:[a-z][a-z_]*\b/gi, '*')
|
22
|
-
.replace(/\{[a-z]+}/gi, '*');
|
22
|
+
.replace(/\{[a-z-.]+}/gi, '*');
|
23
23
|
}
|
24
24
|
|
25
25
|
export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
@@ -69,13 +69,8 @@ export async function writeImageToDisk(systemId: string, event: StormImage, prom
|
|
69
69
|
}
|
70
70
|
|
71
71
|
export function hasPageOnDisk(systemId: string, method: string, path: string) {
|
72
|
-
|
73
|
-
|
74
|
-
}
|
75
|
-
const baseDir = getSystemBaseDir(systemId);
|
76
|
-
const filePath = getFilePath(method);
|
77
|
-
const fullPath = Path.join(baseDir, normalizePath(path), filePath);
|
78
|
-
return FS.existsSync(fullPath);
|
72
|
+
const fullPath = resolveReadPath(systemId, method, path);
|
73
|
+
return !!fullPath && FS.existsSync(fullPath);
|
79
74
|
}
|
80
75
|
|
81
76
|
export function getSystemBaseDir(systemId: string) {
|
@@ -114,7 +109,7 @@ export function resolveReadPath(systemId: string, path: string, method: string)
|
|
114
109
|
|
115
110
|
let currentPath = '';
|
116
111
|
|
117
|
-
for (let part
|
112
|
+
for (let part of parts) {
|
118
113
|
const thisPath = Path.join(currentPath, part);
|
119
114
|
const starPath = Path.join(currentPath, '*');
|
120
115
|
const thisPathDir = Path.join(baseDir, thisPath);
|
@@ -130,7 +125,6 @@ export function resolveReadPath(systemId: string, path: string, method: string)
|
|
130
125
|
continue;
|
131
126
|
}
|
132
127
|
|
133
|
-
console.log('Path not found', thisPathDir, starPathDir);
|
134
128
|
// Path not found
|
135
129
|
return null;
|
136
130
|
}
|
package/src/storm/routes.ts
CHANGED
@@ -102,24 +102,17 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
102
102
|
|
103
103
|
const promises: Promise<void>[] = [];
|
104
104
|
|
105
|
-
queue.on('page', (data) =>
|
106
|
-
if (systemId) {
|
107
|
-
promises.push(sendPageEvent(systemId, data, res));
|
108
|
-
}
|
109
|
-
});
|
105
|
+
queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
|
110
106
|
|
111
|
-
queue.on('image', async (screenData, prompt
|
107
|
+
queue.on('image', async (screenData, prompt) => {
|
112
108
|
if (!systemId) {
|
113
109
|
return;
|
114
110
|
}
|
115
111
|
try {
|
116
|
-
|
117
|
-
promises.push(promise);
|
118
|
-
await promise;
|
119
|
-
future.resolve();
|
112
|
+
await handleImageEvent(systemId, screenData, prompt);
|
120
113
|
} catch (e) {
|
121
114
|
console.error('Failed to handle image event', e);
|
122
|
-
|
115
|
+
throw e;
|
123
116
|
}
|
124
117
|
});
|
125
118
|
|
@@ -229,19 +222,16 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
229
222
|
pageQueue.cancel();
|
230
223
|
});
|
231
224
|
|
232
|
-
pageQueue.on('page', (screenData: StormEventPage) =>
|
233
|
-
|
234
|
-
|
225
|
+
pageQueue.on('page', (screenData: StormEventPage) =>
|
226
|
+
sendPageEvent(landingPagesStream.getConversationId(), screenData, res)
|
227
|
+
);
|
235
228
|
|
236
|
-
pageQueue.on('image', async (screenData, prompt
|
229
|
+
pageQueue.on('image', async (screenData, prompt) => {
|
237
230
|
try {
|
238
|
-
|
239
|
-
pageEventPromises.push(promise);
|
240
|
-
await promise;
|
241
|
-
future.resolve();
|
231
|
+
await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
|
242
232
|
} catch (e) {
|
243
233
|
console.error('Failed to handle image event', e);
|
244
|
-
|
234
|
+
throw e;
|
245
235
|
}
|
246
236
|
});
|
247
237
|
|
@@ -364,7 +354,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
364
354
|
// Get the UI shells
|
365
355
|
const shellsStream = await stormClient.createUIShells(
|
366
356
|
{
|
367
|
-
theme: theme
|
357
|
+
theme: theme || undefined,
|
368
358
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
369
359
|
name: screen.name,
|
370
360
|
title: screen.title,
|
@@ -420,25 +410,18 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
420
410
|
created: Date.now(),
|
421
411
|
});
|
422
412
|
|
423
|
-
const pagePromises: Promise<void>[] = [];
|
424
413
|
onRequestAborted(req, res, () => {
|
425
414
|
queue.cancel();
|
426
415
|
});
|
427
416
|
|
428
|
-
|
429
|
-
queue.on('page', (screenData: StormEventPage) => {
|
430
|
-
pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
|
431
|
-
});
|
417
|
+
queue.on('page', (screenData: StormEventPage) => sendPageEvent(outerConversationId, screenData, res));
|
432
418
|
|
433
|
-
queue.on('image', async (screenData, prompt
|
419
|
+
queue.on('image', async (screenData, prompt) => {
|
434
420
|
try {
|
435
|
-
|
436
|
-
pageEventPromises.push(promise);
|
437
|
-
await promise;
|
438
|
-
future.resolve();
|
421
|
+
await handleImageEvent(outerConversationId, screenData, prompt);
|
439
422
|
} catch (e) {
|
440
423
|
console.error('Failed to handle image event', e);
|
441
|
-
|
424
|
+
throw e;
|
442
425
|
}
|
443
426
|
});
|
444
427
|
|
@@ -447,8 +430,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
447
430
|
});
|
448
431
|
|
449
432
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
450
|
-
|
451
|
-
|
433
|
+
queue
|
434
|
+
.addPrompt({
|
452
435
|
prompt: screen.requirements,
|
453
436
|
method: screen.method,
|
454
437
|
path: screen.path,
|
@@ -459,7 +442,9 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
459
442
|
storage_prefix: outerConversationId + '_',
|
460
443
|
theme,
|
461
444
|
})
|
462
|
-
|
445
|
+
.catch((e) => {
|
446
|
+
console.error('Failed to generate page for screen %s', screen.name, e);
|
447
|
+
});
|
463
448
|
}
|
464
449
|
|
465
450
|
if (userJourneysStream.isAborted()) {
|
@@ -467,8 +452,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
467
452
|
}
|
468
453
|
|
469
454
|
await queue.wait();
|
470
|
-
await Promise.allSettled(pagePromises);
|
471
|
-
await Promise.allSettled(pageEventPromises);
|
472
455
|
|
473
456
|
sendDone(res);
|
474
457
|
} catch (err) {
|