@kapeta/local-cluster-service 0.67.4 → 0.68.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 +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 +93 -13
- package/dist/cjs/src/storm/routes.js +18 -35
- 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 +93 -13
- package/dist/esm/src/storm/routes.js +18 -35
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +63 -73
- package/src/storm/page-utils.ts +93 -13
- package/src/storm/routes.ts +19 -36
@@ -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
|
}
|
@@ -121,15 +115,101 @@ exports.readPageFromDiskAsString = readPageFromDiskAsString;
|
|
121
115
|
function readPageFromDisk(systemId, path, method, res) {
|
122
116
|
const filePath = resolveReadPath(systemId, path, method);
|
123
117
|
if (!filePath || !fs_extra_1.default.existsSync(filePath)) {
|
124
|
-
|
118
|
+
if (method === 'HEAD') {
|
119
|
+
// For HEAD requests, only return the status and headers
|
120
|
+
res.status(202).set('Retry-After', '3').end();
|
121
|
+
}
|
122
|
+
else {
|
123
|
+
// For GET requests, return the fallback HTML with status 202
|
124
|
+
res.status(202).set('Retry-After', '3').send(getFallbackHtml(path, method));
|
125
|
+
}
|
125
126
|
return;
|
126
127
|
}
|
127
128
|
res.type(filePath.split('.').pop());
|
128
129
|
const content = fs_extra_1.default.readFileSync(filePath);
|
129
|
-
|
130
|
-
|
130
|
+
if (method === 'HEAD') {
|
131
|
+
// For HEAD requests, just end the response after setting headers
|
132
|
+
res.status(200).end();
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
// For GET requests, return the full content
|
136
|
+
res.write(content);
|
137
|
+
res.end();
|
138
|
+
}
|
131
139
|
}
|
132
140
|
exports.readPageFromDisk = readPageFromDisk;
|
141
|
+
function getFallbackHtml(path, method) {
|
142
|
+
return `
|
143
|
+
<!DOCTYPE html>
|
144
|
+
<html lang="en">
|
145
|
+
|
146
|
+
<head>
|
147
|
+
<meta charset="UTF-8">
|
148
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
149
|
+
<title>Page Not Ready</title>
|
150
|
+
<style>
|
151
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap');
|
152
|
+
|
153
|
+
body {
|
154
|
+
margin: 0;
|
155
|
+
padding: 0;
|
156
|
+
display: flex;
|
157
|
+
align-items: center;
|
158
|
+
justify-content: center;
|
159
|
+
height: 100vh;
|
160
|
+
font-family: 'Roboto', sans-serif;
|
161
|
+
background: #1E1F20;
|
162
|
+
color: white;
|
163
|
+
text-align: center;
|
164
|
+
}
|
165
|
+
|
166
|
+
h1 {
|
167
|
+
font-size: 2rem;
|
168
|
+
font-weight: 600;
|
169
|
+
margin-bottom: 1rem;
|
170
|
+
}
|
171
|
+
|
172
|
+
p {
|
173
|
+
font-size: 1rem;
|
174
|
+
font-weight: 400;
|
175
|
+
}
|
176
|
+
</style>
|
177
|
+
</head>
|
178
|
+
|
179
|
+
<body>
|
180
|
+
<div>
|
181
|
+
<h1>Page Not Ready</h1>
|
182
|
+
<p>Henrik is still working on this page. Please wait...</p>
|
183
|
+
</div>
|
184
|
+
<script>
|
185
|
+
const checkInterval = 3000;
|
186
|
+
function checkPageReady() {
|
187
|
+
fetch('${path}', { method: 'HEAD' })
|
188
|
+
.then(response => {
|
189
|
+
if (response.status === 200) {
|
190
|
+
// The page is ready, reload to fetch it
|
191
|
+
window.location.reload();
|
192
|
+
} else if (response.status === 202) {
|
193
|
+
const retryAfter = response.headers.get('Retry-After');
|
194
|
+
const retryInterval = retryAfter ? parseInt(retryAfter) * 1000 : 3000;
|
195
|
+
setTimeout(checkPageReady, retryInterval);
|
196
|
+
} else {
|
197
|
+
// Handle other unexpected statuses
|
198
|
+
setTimeout(checkPageReady, 3000);
|
199
|
+
}
|
200
|
+
})
|
201
|
+
.catch(error => {
|
202
|
+
console.error('Error checking page status:', error);
|
203
|
+
setTimeout(checkPageReady, checkInterval);
|
204
|
+
});
|
205
|
+
}
|
206
|
+
setTimeout(checkPageReady, checkInterval);
|
207
|
+
</script>
|
208
|
+
</body>
|
209
|
+
|
210
|
+
</html>
|
211
|
+
`;
|
212
|
+
}
|
133
213
|
function readConversationFromFile(filename) {
|
134
214
|
if (!fs_extra_1.default.existsSync(filename)) {
|
135
215
|
return [];
|
@@ -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) => {
|
@@ -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) {
|