@kapeta/local-cluster-service 0.70.5 → 0.70.7
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 +17 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/src/storm/PageGenerator.d.ts +2 -0
- package/dist/cjs/src/storm/PageGenerator.js +53 -6
- package/dist/cjs/src/storm/routes.js +16 -0
- package/dist/cjs/src/storm/stormClient.d.ts +1 -2
- package/dist/cjs/src/storm/stormClient.js +22 -14
- package/dist/esm/index.js +1 -1
- package/dist/esm/src/storm/PageGenerator.d.ts +2 -0
- package/dist/esm/src/storm/PageGenerator.js +53 -6
- package/dist/esm/src/storm/routes.js +16 -0
- package/dist/esm/src/storm/stormClient.d.ts +1 -2
- package/dist/esm/src/storm/stormClient.js +22 -14
- package/index.ts +1 -1
- package/package.json +4 -1
- package/src/storm/PageGenerator.ts +34 -10
- package/src/storm/routes.ts +20 -0
- package/src/storm/stormClient.ts +21 -12
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## [0.70.7](https://github.com/kapetacom/local-cluster-service/compare/v0.70.6...v0.70.7) (2024-09-12)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* add missing error handlers to pagegen ([56a3ab5](https://github.com/kapetacom/local-cluster-service/commit/56a3ab576f011ed29ce8074c53e025b347fb4e8a))
|
7
|
+
* skip unknown mimetypes in image gen ([3076367](https://github.com/kapetacom/local-cluster-service/commit/307636771c46a99f555c4c3e944c61fcffa16adc))
|
8
|
+
|
9
|
+
## [0.70.6](https://github.com/kapetacom/local-cluster-service/compare/v0.70.5...v0.70.6) (2024-09-11)
|
10
|
+
|
11
|
+
|
12
|
+
### Bug Fixes
|
13
|
+
|
14
|
+
* disable sentry in dev mode ([9a7793f](https://github.com/kapetacom/local-cluster-service/commit/9a7793feaf81239d600d2b6650f126780f0ffdd5))
|
15
|
+
* emit errors to res instead of into void ([d1302c1](https://github.com/kapetacom/local-cluster-service/commit/d1302c1b523a72de6596b619a44a2cde073f032f))
|
16
|
+
* promise error handling and fetch retries ([d746e14](https://github.com/kapetacom/local-cluster-service/commit/d746e14c1edaaddd1a3aa5cb7bbf644dfde334b2))
|
17
|
+
|
1
18
|
## [0.70.5](https://github.com/kapetacom/local-cluster-service/compare/v0.70.4...v0.70.5) (2024-09-11)
|
2
19
|
|
3
20
|
|
package/dist/cjs/index.js
CHANGED
@@ -64,7 +64,7 @@ const assetManager_1 = require("./src/assetManager");
|
|
64
64
|
const instanceManager_1 = require("./src/instanceManager");
|
65
65
|
Sentry.init({
|
66
66
|
dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
|
67
|
-
enabled: process.env.NODE_ENV !== 'development',
|
67
|
+
enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
|
68
68
|
// Performance Monitoring on every ~20th request
|
69
69
|
tracesSampleRate: 0.05,
|
70
70
|
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
@@ -29,9 +29,11 @@ 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: 'error', listener: (error: unknown) => void): this;
|
32
33
|
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
33
34
|
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
34
35
|
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
36
|
+
emit(type: 'error', error: unknown): boolean;
|
35
37
|
emit(type: 'event', event: StormEvent): boolean;
|
36
38
|
emit(type: 'page', event: StormEventPage): boolean;
|
37
39
|
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
@@ -3,6 +3,29 @@
|
|
3
3
|
* Copyright 2023 Kapeta Inc.
|
4
4
|
* SPDX-License-Identifier: BUSL-1.1
|
5
5
|
*/
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
7
|
+
if (k2 === undefined) k2 = k;
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
11
|
+
}
|
12
|
+
Object.defineProperty(o, k2, desc);
|
13
|
+
}) : (function(o, m, k, k2) {
|
14
|
+
if (k2 === undefined) k2 = k;
|
15
|
+
o[k2] = m[k];
|
16
|
+
}));
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
19
|
+
}) : function(o, v) {
|
20
|
+
o["default"] = v;
|
21
|
+
});
|
22
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
23
|
+
if (mod && mod.__esModule) return mod;
|
24
|
+
var result = {};
|
25
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
26
|
+
__setModuleDefault(result, mod);
|
27
|
+
return result;
|
28
|
+
};
|
6
29
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
7
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
31
|
};
|
@@ -13,6 +36,7 @@ const stormClient_1 = require("./stormClient");
|
|
13
36
|
const node_events_1 = require("node:events");
|
14
37
|
const p_queue_1 = __importDefault(require("p-queue"));
|
15
38
|
const page_utils_1 = require("./page-utils");
|
39
|
+
const mimetypes = __importStar(require("mime-types"));
|
16
40
|
class PageQueue extends node_events_1.EventEmitter {
|
17
41
|
queue;
|
18
42
|
eventQueue;
|
@@ -32,7 +56,12 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
32
56
|
}
|
33
57
|
on(event, listener) {
|
34
58
|
return super.on(event, (...args) => {
|
35
|
-
|
59
|
+
this.eventQueue
|
60
|
+
.add(async () => listener(...args))
|
61
|
+
// If the event queue fails, we want to emit an error
|
62
|
+
.catch((err) => {
|
63
|
+
this.emit('error', err);
|
64
|
+
});
|
36
65
|
});
|
37
66
|
}
|
38
67
|
emit(eventName, ...args) {
|
@@ -121,6 +150,9 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
121
150
|
await this.addImagePrompt({
|
122
151
|
...reference,
|
123
152
|
content: event.payload.content,
|
153
|
+
}).catch((err) => {
|
154
|
+
console.error('Failed to generate image for reference', reference.name, err);
|
155
|
+
this.emit('error', err);
|
124
156
|
});
|
125
157
|
break;
|
126
158
|
case 'css':
|
@@ -154,7 +186,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
154
186
|
await Promise.allSettled(resourcePromises);
|
155
187
|
this.emit('page', event);
|
156
188
|
// Emit any new pages after the current page to increase responsiveness
|
157
|
-
|
189
|
+
initialPrompts.forEach((prompt) => {
|
158
190
|
if (!this.hasPrompt(prompt.path)) {
|
159
191
|
this.emit('page', {
|
160
192
|
type: 'PAGE',
|
@@ -173,7 +205,11 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
173
205
|
},
|
174
206
|
});
|
175
207
|
}
|
176
|
-
|
208
|
+
// Trigger but don't wait for the "bonus" pages
|
209
|
+
this.addPrompt(prompt).catch((err) => {
|
210
|
+
console.error('Failed to generate page reference', prompt.name, err);
|
211
|
+
this.emit('error', err);
|
212
|
+
});
|
177
213
|
});
|
178
214
|
}
|
179
215
|
catch (e) {
|
@@ -196,6 +232,14 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
196
232
|
//console.log('Ignoring duplicate image prompt', prompt);
|
197
233
|
return;
|
198
234
|
}
|
235
|
+
// Add safeguard to avoid generating images for nonsense URLs
|
236
|
+
// Sometimes we get entries for Base URLs that will then cause issues on the filesystem
|
237
|
+
// Example: https://www.kapeta.com/images/
|
238
|
+
const mimeType = mimetypes.lookup(prompt.url);
|
239
|
+
if (!mimeType || !mimeType.startsWith('image/')) {
|
240
|
+
console.warn('Skipping image reference of type %s for url %s', mimeType, prompt.url);
|
241
|
+
return;
|
242
|
+
}
|
199
243
|
this.images.set(prompt.url, prompt.description);
|
200
244
|
const prefix = this.getPrefix();
|
201
245
|
const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
|
@@ -208,18 +252,21 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
208
252
|
await result.waitForDone();
|
209
253
|
}
|
210
254
|
async generate(prompt, conversationId) {
|
211
|
-
const promises = [];
|
212
255
|
const screenStream = await stormClient_1.stormClient.createUIPage(prompt, conversationId);
|
256
|
+
let pageEvent = null;
|
213
257
|
screenStream.on('data', (event) => {
|
214
258
|
if (event.type === 'PAGE') {
|
215
259
|
event.payload.conversationId = conversationId;
|
216
|
-
|
260
|
+
pageEvent = event;
|
217
261
|
return;
|
218
262
|
}
|
219
263
|
this.emit('event', event);
|
220
264
|
});
|
221
265
|
await screenStream.waitForDone();
|
222
|
-
|
266
|
+
if (!pageEvent) {
|
267
|
+
throw new Error('No page was generated');
|
268
|
+
}
|
269
|
+
await this.processPageEventWithReferences(pageEvent);
|
223
270
|
}
|
224
271
|
async resolveReferences(content) {
|
225
272
|
const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
|
@@ -124,6 +124,10 @@ router.post('/ui/screen', async (req, res) => {
|
|
124
124
|
throw e;
|
125
125
|
}
|
126
126
|
});
|
127
|
+
queue.on('error', (err) => {
|
128
|
+
console.error('Failed to process page', err);
|
129
|
+
sendError(err, res);
|
130
|
+
});
|
127
131
|
await queue.addPrompt(aiRequest, conversationId, true);
|
128
132
|
await queue.wait();
|
129
133
|
await Promise.allSettled(promises);
|
@@ -214,6 +218,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
214
218
|
pageQueue.on('event', (screenData) => {
|
215
219
|
sendEvent(res, screenData);
|
216
220
|
});
|
221
|
+
pageQueue.on('error', (err) => {
|
222
|
+
console.error('Failed to process page', err);
|
223
|
+
sendError(err, res);
|
224
|
+
});
|
217
225
|
await waitForStormStream(landingPagesStream);
|
218
226
|
await pageQueue.wait();
|
219
227
|
await Promise.allSettled(pageEventPromises);
|
@@ -372,6 +380,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
372
380
|
queue.on('event', (screenData) => {
|
373
381
|
sendEvent(res, screenData);
|
374
382
|
});
|
383
|
+
queue.on('error', (err) => {
|
384
|
+
console.error('Failed to process page', err);
|
385
|
+
sendError(err, res);
|
386
|
+
});
|
375
387
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
376
388
|
queue
|
377
389
|
.addPrompt({
|
@@ -427,6 +439,10 @@ router.post('/ui/edit', async (req, res) => {
|
|
427
439
|
sendEvent(res, data);
|
428
440
|
}
|
429
441
|
});
|
442
|
+
queue.on('error', (err) => {
|
443
|
+
console.error('Failed to process page', err);
|
444
|
+
sendError(err, res);
|
445
|
+
});
|
430
446
|
const pages = aiRequest.prompt.pages.filter((page) => page.conversationId);
|
431
447
|
if (pages.length === 0) {
|
432
448
|
console.log('No pages to update', aiRequest.prompt.pages);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
/// <reference types="node" />
|
2
1
|
import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
|
3
2
|
import { Page, StormEventPageUrl } from './events';
|
4
3
|
export declare const STORM_ID = "storm";
|
@@ -66,7 +65,7 @@ declare class StormClient {
|
|
66
65
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
67
66
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
68
67
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
69
|
-
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
|
68
|
+
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
|
70
69
|
getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
|
71
70
|
vote: -1 | 0 | 1;
|
72
71
|
}>;
|
@@ -13,6 +13,22 @@ const utils_1 = require("../utils/utils");
|
|
13
13
|
const promises_1 = __importDefault(require("node:readline/promises"));
|
14
14
|
const node_stream_1 = require("node:stream");
|
15
15
|
const stream_1 = require("./stream");
|
16
|
+
const undici_1 = require("undici");
|
17
|
+
// Will only retry on error codes and GET requests by default
|
18
|
+
// See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
|
19
|
+
const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
|
20
|
+
methods: [
|
21
|
+
// Added methods ↓ (not idempotent), but we want to retry on POST:
|
22
|
+
'POST',
|
23
|
+
// defaults below
|
24
|
+
'GET',
|
25
|
+
'HEAD',
|
26
|
+
'OPTIONS',
|
27
|
+
'PUT',
|
28
|
+
'DELETE',
|
29
|
+
'TRACE',
|
30
|
+
],
|
31
|
+
});
|
16
32
|
exports.STORM_ID = 'storm';
|
17
33
|
exports.ConversationIdHeader = 'Conversation-Id';
|
18
34
|
class StormClient {
|
@@ -38,6 +54,7 @@ class StormClient {
|
|
38
54
|
method: method,
|
39
55
|
body: JSON.stringify(body),
|
40
56
|
headers,
|
57
|
+
dispatcher: retryAgent,
|
41
58
|
};
|
42
59
|
}
|
43
60
|
async send(path, body, method = 'POST') {
|
@@ -48,7 +65,7 @@ class StormClient {
|
|
48
65
|
});
|
49
66
|
const abort = new AbortController();
|
50
67
|
options.signal = abort.signal;
|
51
|
-
const response = await fetch(options.url, options);
|
68
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
52
69
|
if (response.status !== 200) {
|
53
70
|
throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
|
54
71
|
}
|
@@ -121,25 +138,19 @@ class StormClient {
|
|
121
138
|
prompt: JSON.stringify({ topic, vote, mainConversationId }),
|
122
139
|
conversationId,
|
123
140
|
});
|
124
|
-
return fetch(options.url,
|
125
|
-
method: options.method,
|
126
|
-
headers: options.headers,
|
127
|
-
});
|
141
|
+
return (0, undici_1.fetch)(options.url, options);
|
128
142
|
}
|
129
143
|
async getVoteUIPage(topic, conversationId, mainConversationId) {
|
130
144
|
const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
|
131
145
|
prompt: JSON.stringify({ topic, mainConversationId }),
|
132
146
|
conversationId,
|
133
147
|
});
|
134
|
-
const response = await fetch(options.url,
|
135
|
-
method: options.method,
|
136
|
-
headers: options.headers,
|
137
|
-
});
|
148
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
138
149
|
return response.json();
|
139
150
|
}
|
140
151
|
async implementAPIClients(prompt) {
|
141
152
|
const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
|
142
|
-
const response = await fetch(u, {
|
153
|
+
const response = await (0, undici_1.fetch)(u, {
|
143
154
|
method: 'POST',
|
144
155
|
body: JSON.stringify({
|
145
156
|
fileName: prompt.fileName,
|
@@ -214,10 +225,7 @@ class StormClient {
|
|
214
225
|
prompt: '',
|
215
226
|
conversationId: conversationId,
|
216
227
|
});
|
217
|
-
const response = await fetch(options.url,
|
218
|
-
method: options.method,
|
219
|
-
headers: options.headers,
|
220
|
-
});
|
228
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
221
229
|
return response.text();
|
222
230
|
}
|
223
231
|
}
|
package/dist/esm/index.js
CHANGED
@@ -64,7 +64,7 @@ const assetManager_1 = require("./src/assetManager");
|
|
64
64
|
const instanceManager_1 = require("./src/instanceManager");
|
65
65
|
Sentry.init({
|
66
66
|
dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
|
67
|
-
enabled: process.env.NODE_ENV !== 'development',
|
67
|
+
enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
|
68
68
|
// Performance Monitoring on every ~20th request
|
69
69
|
tracesSampleRate: 0.05,
|
70
70
|
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
@@ -29,9 +29,11 @@ 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: 'error', listener: (error: unknown) => void): this;
|
32
33
|
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
33
34
|
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
34
35
|
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
36
|
+
emit(type: 'error', error: unknown): boolean;
|
35
37
|
emit(type: 'event', event: StormEvent): boolean;
|
36
38
|
emit(type: 'page', event: StormEventPage): boolean;
|
37
39
|
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
@@ -3,6 +3,29 @@
|
|
3
3
|
* Copyright 2023 Kapeta Inc.
|
4
4
|
* SPDX-License-Identifier: BUSL-1.1
|
5
5
|
*/
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
7
|
+
if (k2 === undefined) k2 = k;
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
11
|
+
}
|
12
|
+
Object.defineProperty(o, k2, desc);
|
13
|
+
}) : (function(o, m, k, k2) {
|
14
|
+
if (k2 === undefined) k2 = k;
|
15
|
+
o[k2] = m[k];
|
16
|
+
}));
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
19
|
+
}) : function(o, v) {
|
20
|
+
o["default"] = v;
|
21
|
+
});
|
22
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
23
|
+
if (mod && mod.__esModule) return mod;
|
24
|
+
var result = {};
|
25
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
26
|
+
__setModuleDefault(result, mod);
|
27
|
+
return result;
|
28
|
+
};
|
6
29
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
7
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
31
|
};
|
@@ -13,6 +36,7 @@ const stormClient_1 = require("./stormClient");
|
|
13
36
|
const node_events_1 = require("node:events");
|
14
37
|
const p_queue_1 = __importDefault(require("p-queue"));
|
15
38
|
const page_utils_1 = require("./page-utils");
|
39
|
+
const mimetypes = __importStar(require("mime-types"));
|
16
40
|
class PageQueue extends node_events_1.EventEmitter {
|
17
41
|
queue;
|
18
42
|
eventQueue;
|
@@ -32,7 +56,12 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
32
56
|
}
|
33
57
|
on(event, listener) {
|
34
58
|
return super.on(event, (...args) => {
|
35
|
-
|
59
|
+
this.eventQueue
|
60
|
+
.add(async () => listener(...args))
|
61
|
+
// If the event queue fails, we want to emit an error
|
62
|
+
.catch((err) => {
|
63
|
+
this.emit('error', err);
|
64
|
+
});
|
36
65
|
});
|
37
66
|
}
|
38
67
|
emit(eventName, ...args) {
|
@@ -121,6 +150,9 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
121
150
|
await this.addImagePrompt({
|
122
151
|
...reference,
|
123
152
|
content: event.payload.content,
|
153
|
+
}).catch((err) => {
|
154
|
+
console.error('Failed to generate image for reference', reference.name, err);
|
155
|
+
this.emit('error', err);
|
124
156
|
});
|
125
157
|
break;
|
126
158
|
case 'css':
|
@@ -154,7 +186,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
154
186
|
await Promise.allSettled(resourcePromises);
|
155
187
|
this.emit('page', event);
|
156
188
|
// Emit any new pages after the current page to increase responsiveness
|
157
|
-
|
189
|
+
initialPrompts.forEach((prompt) => {
|
158
190
|
if (!this.hasPrompt(prompt.path)) {
|
159
191
|
this.emit('page', {
|
160
192
|
type: 'PAGE',
|
@@ -173,7 +205,11 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
173
205
|
},
|
174
206
|
});
|
175
207
|
}
|
176
|
-
|
208
|
+
// Trigger but don't wait for the "bonus" pages
|
209
|
+
this.addPrompt(prompt).catch((err) => {
|
210
|
+
console.error('Failed to generate page reference', prompt.name, err);
|
211
|
+
this.emit('error', err);
|
212
|
+
});
|
177
213
|
});
|
178
214
|
}
|
179
215
|
catch (e) {
|
@@ -196,6 +232,14 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
196
232
|
//console.log('Ignoring duplicate image prompt', prompt);
|
197
233
|
return;
|
198
234
|
}
|
235
|
+
// Add safeguard to avoid generating images for nonsense URLs
|
236
|
+
// Sometimes we get entries for Base URLs that will then cause issues on the filesystem
|
237
|
+
// Example: https://www.kapeta.com/images/
|
238
|
+
const mimeType = mimetypes.lookup(prompt.url);
|
239
|
+
if (!mimeType || !mimeType.startsWith('image/')) {
|
240
|
+
console.warn('Skipping image reference of type %s for url %s', mimeType, prompt.url);
|
241
|
+
return;
|
242
|
+
}
|
199
243
|
this.images.set(prompt.url, prompt.description);
|
200
244
|
const prefix = this.getPrefix();
|
201
245
|
const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
|
@@ -208,18 +252,21 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
208
252
|
await result.waitForDone();
|
209
253
|
}
|
210
254
|
async generate(prompt, conversationId) {
|
211
|
-
const promises = [];
|
212
255
|
const screenStream = await stormClient_1.stormClient.createUIPage(prompt, conversationId);
|
256
|
+
let pageEvent = null;
|
213
257
|
screenStream.on('data', (event) => {
|
214
258
|
if (event.type === 'PAGE') {
|
215
259
|
event.payload.conversationId = conversationId;
|
216
|
-
|
260
|
+
pageEvent = event;
|
217
261
|
return;
|
218
262
|
}
|
219
263
|
this.emit('event', event);
|
220
264
|
});
|
221
265
|
await screenStream.waitForDone();
|
222
|
-
|
266
|
+
if (!pageEvent) {
|
267
|
+
throw new Error('No page was generated');
|
268
|
+
}
|
269
|
+
await this.processPageEventWithReferences(pageEvent);
|
223
270
|
}
|
224
271
|
async resolveReferences(content) {
|
225
272
|
const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
|
@@ -124,6 +124,10 @@ router.post('/ui/screen', async (req, res) => {
|
|
124
124
|
throw e;
|
125
125
|
}
|
126
126
|
});
|
127
|
+
queue.on('error', (err) => {
|
128
|
+
console.error('Failed to process page', err);
|
129
|
+
sendError(err, res);
|
130
|
+
});
|
127
131
|
await queue.addPrompt(aiRequest, conversationId, true);
|
128
132
|
await queue.wait();
|
129
133
|
await Promise.allSettled(promises);
|
@@ -214,6 +218,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
214
218
|
pageQueue.on('event', (screenData) => {
|
215
219
|
sendEvent(res, screenData);
|
216
220
|
});
|
221
|
+
pageQueue.on('error', (err) => {
|
222
|
+
console.error('Failed to process page', err);
|
223
|
+
sendError(err, res);
|
224
|
+
});
|
217
225
|
await waitForStormStream(landingPagesStream);
|
218
226
|
await pageQueue.wait();
|
219
227
|
await Promise.allSettled(pageEventPromises);
|
@@ -372,6 +380,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
372
380
|
queue.on('event', (screenData) => {
|
373
381
|
sendEvent(res, screenData);
|
374
382
|
});
|
383
|
+
queue.on('error', (err) => {
|
384
|
+
console.error('Failed to process page', err);
|
385
|
+
sendError(err, res);
|
386
|
+
});
|
375
387
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
376
388
|
queue
|
377
389
|
.addPrompt({
|
@@ -427,6 +439,10 @@ router.post('/ui/edit', async (req, res) => {
|
|
427
439
|
sendEvent(res, data);
|
428
440
|
}
|
429
441
|
});
|
442
|
+
queue.on('error', (err) => {
|
443
|
+
console.error('Failed to process page', err);
|
444
|
+
sendError(err, res);
|
445
|
+
});
|
430
446
|
const pages = aiRequest.prompt.pages.filter((page) => page.conversationId);
|
431
447
|
if (pages.length === 0) {
|
432
448
|
console.log('No pages to update', aiRequest.prompt.pages);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
/// <reference types="node" />
|
2
1
|
import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
|
3
2
|
import { Page, StormEventPageUrl } from './events';
|
4
3
|
export declare const STORM_ID = "storm";
|
@@ -66,7 +65,7 @@ declare class StormClient {
|
|
66
65
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
67
66
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
68
67
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
69
|
-
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
|
68
|
+
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
|
70
69
|
getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
|
71
70
|
vote: -1 | 0 | 1;
|
72
71
|
}>;
|
@@ -13,6 +13,22 @@ const utils_1 = require("../utils/utils");
|
|
13
13
|
const promises_1 = __importDefault(require("node:readline/promises"));
|
14
14
|
const node_stream_1 = require("node:stream");
|
15
15
|
const stream_1 = require("./stream");
|
16
|
+
const undici_1 = require("undici");
|
17
|
+
// Will only retry on error codes and GET requests by default
|
18
|
+
// See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
|
19
|
+
const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
|
20
|
+
methods: [
|
21
|
+
// Added methods ↓ (not idempotent), but we want to retry on POST:
|
22
|
+
'POST',
|
23
|
+
// defaults below
|
24
|
+
'GET',
|
25
|
+
'HEAD',
|
26
|
+
'OPTIONS',
|
27
|
+
'PUT',
|
28
|
+
'DELETE',
|
29
|
+
'TRACE',
|
30
|
+
],
|
31
|
+
});
|
16
32
|
exports.STORM_ID = 'storm';
|
17
33
|
exports.ConversationIdHeader = 'Conversation-Id';
|
18
34
|
class StormClient {
|
@@ -38,6 +54,7 @@ class StormClient {
|
|
38
54
|
method: method,
|
39
55
|
body: JSON.stringify(body),
|
40
56
|
headers,
|
57
|
+
dispatcher: retryAgent,
|
41
58
|
};
|
42
59
|
}
|
43
60
|
async send(path, body, method = 'POST') {
|
@@ -48,7 +65,7 @@ class StormClient {
|
|
48
65
|
});
|
49
66
|
const abort = new AbortController();
|
50
67
|
options.signal = abort.signal;
|
51
|
-
const response = await fetch(options.url, options);
|
68
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
52
69
|
if (response.status !== 200) {
|
53
70
|
throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
|
54
71
|
}
|
@@ -121,25 +138,19 @@ class StormClient {
|
|
121
138
|
prompt: JSON.stringify({ topic, vote, mainConversationId }),
|
122
139
|
conversationId,
|
123
140
|
});
|
124
|
-
return fetch(options.url,
|
125
|
-
method: options.method,
|
126
|
-
headers: options.headers,
|
127
|
-
});
|
141
|
+
return (0, undici_1.fetch)(options.url, options);
|
128
142
|
}
|
129
143
|
async getVoteUIPage(topic, conversationId, mainConversationId) {
|
130
144
|
const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
|
131
145
|
prompt: JSON.stringify({ topic, mainConversationId }),
|
132
146
|
conversationId,
|
133
147
|
});
|
134
|
-
const response = await fetch(options.url,
|
135
|
-
method: options.method,
|
136
|
-
headers: options.headers,
|
137
|
-
});
|
148
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
138
149
|
return response.json();
|
139
150
|
}
|
140
151
|
async implementAPIClients(prompt) {
|
141
152
|
const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
|
142
|
-
const response = await fetch(u, {
|
153
|
+
const response = await (0, undici_1.fetch)(u, {
|
143
154
|
method: 'POST',
|
144
155
|
body: JSON.stringify({
|
145
156
|
fileName: prompt.fileName,
|
@@ -214,10 +225,7 @@ class StormClient {
|
|
214
225
|
prompt: '',
|
215
226
|
conversationId: conversationId,
|
216
227
|
});
|
217
|
-
const response = await fetch(options.url,
|
218
|
-
method: options.method,
|
219
|
-
headers: options.headers,
|
220
|
-
});
|
228
|
+
const response = await (0, undici_1.fetch)(options.url, options);
|
221
229
|
return response.text();
|
222
230
|
}
|
223
231
|
}
|
package/index.ts
CHANGED
@@ -39,7 +39,7 @@ import { instanceManager } from './src/instanceManager';
|
|
39
39
|
|
40
40
|
Sentry.init({
|
41
41
|
dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
|
42
|
-
enabled: process.env.NODE_ENV !== 'development',
|
42
|
+
enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
|
43
43
|
// Performance Monitoring on every ~20th request
|
44
44
|
tracesSampleRate: 0.05,
|
45
45
|
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.70.
|
3
|
+
"version": "0.70.7",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -76,6 +76,7 @@
|
|
76
76
|
"js-yaml": "^4.1.0",
|
77
77
|
"lodash": "^4.17.15",
|
78
78
|
"md5": "2.2.1",
|
79
|
+
"mime-types": "^2.1.35",
|
79
80
|
"node-cache": "^5.1.2",
|
80
81
|
"node-fetch": "^3.3.2",
|
81
82
|
"node-uuid": "^1.4.8",
|
@@ -87,6 +88,7 @@
|
|
87
88
|
"stream-json": "^1.8.0",
|
88
89
|
"tar-stream": "^3.1.6",
|
89
90
|
"typescript": "^5.1.6",
|
91
|
+
"undici": "^6.19.8",
|
90
92
|
"uuid": "^9.0.1",
|
91
93
|
"yaml": "^1.6.0"
|
92
94
|
},
|
@@ -103,6 +105,7 @@
|
|
103
105
|
"@types/js-yaml": "^4.0.9",
|
104
106
|
"@types/lodash": "^4.14.195",
|
105
107
|
"@types/md5": "^2.3.2",
|
108
|
+
"@types/mime-types": "^2.1.4",
|
106
109
|
"@types/node": "^20.5.8",
|
107
110
|
"@types/node-fetch": "^2.6.11",
|
108
111
|
"@types/node-uuid": "^0.0.29",
|
@@ -10,6 +10,7 @@ import { EventEmitter } from 'node:events';
|
|
10
10
|
import PQueue from 'p-queue';
|
11
11
|
|
12
12
|
import { hasPageOnDisk, normalizePath } from './page-utils';
|
13
|
+
import * as mimetypes from 'mime-types';
|
13
14
|
|
14
15
|
export interface ImagePrompt {
|
15
16
|
name: string;
|
@@ -42,16 +43,23 @@ export class PageQueue extends EventEmitter {
|
|
42
43
|
this.eventQueue = new PQueue({ concurrency: Number.MAX_VALUE });
|
43
44
|
}
|
44
45
|
|
46
|
+
on(event: 'error', listener: (error: unknown) => void): this;
|
45
47
|
on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
|
46
48
|
on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
|
47
49
|
on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
|
48
50
|
|
49
51
|
on(event: string, listener: (...args: any[]) => void | Promise<void>): this {
|
50
52
|
return super.on(event, (...args) => {
|
51
|
-
|
53
|
+
this.eventQueue
|
54
|
+
.add(async () => listener(...args))
|
55
|
+
// If the event queue fails, we want to emit an error
|
56
|
+
.catch((err) => {
|
57
|
+
this.emit('error', err);
|
58
|
+
});
|
52
59
|
});
|
53
60
|
}
|
54
61
|
|
62
|
+
emit(type: 'error', error: unknown): boolean;
|
55
63
|
emit(type: 'event', event: StormEvent): boolean;
|
56
64
|
emit(type: 'page', event: StormEventPage): boolean;
|
57
65
|
emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
|
@@ -160,6 +168,9 @@ export class PageQueue extends EventEmitter {
|
|
160
168
|
await this.addImagePrompt({
|
161
169
|
...reference,
|
162
170
|
content: event.payload.content,
|
171
|
+
}).catch((err) => {
|
172
|
+
console.error('Failed to generate image for reference', reference.name, err);
|
173
|
+
this.emit('error', err);
|
163
174
|
});
|
164
175
|
break;
|
165
176
|
case 'css':
|
@@ -197,7 +208,7 @@ export class PageQueue extends EventEmitter {
|
|
197
208
|
this.emit('page', event);
|
198
209
|
|
199
210
|
// Emit any new pages after the current page to increase responsiveness
|
200
|
-
|
211
|
+
initialPrompts.forEach((prompt) => {
|
201
212
|
if (!this.hasPrompt(prompt.path)) {
|
202
213
|
this.emit('page', {
|
203
214
|
type: 'PAGE',
|
@@ -216,7 +227,11 @@ export class PageQueue extends EventEmitter {
|
|
216
227
|
},
|
217
228
|
});
|
218
229
|
}
|
219
|
-
|
230
|
+
// Trigger but don't wait for the "bonus" pages
|
231
|
+
this.addPrompt(prompt).catch((err) => {
|
232
|
+
console.error('Failed to generate page reference', prompt.name, err);
|
233
|
+
this.emit('error', err);
|
234
|
+
});
|
220
235
|
});
|
221
236
|
} catch (e) {
|
222
237
|
console.error('Failed to process event', e);
|
@@ -241,6 +256,15 @@ export class PageQueue extends EventEmitter {
|
|
241
256
|
//console.log('Ignoring duplicate image prompt', prompt);
|
242
257
|
return;
|
243
258
|
}
|
259
|
+
// Add safeguard to avoid generating images for nonsense URLs
|
260
|
+
// Sometimes we get entries for Base URLs that will then cause issues on the filesystem
|
261
|
+
// Example: https://www.kapeta.com/images/
|
262
|
+
const mimeType = mimetypes.lookup(prompt.url) as string | false;
|
263
|
+
if (!mimeType || !mimeType.startsWith('image/')) {
|
264
|
+
console.warn('Skipping image reference of type %s for url %s', mimeType, prompt.url);
|
265
|
+
return;
|
266
|
+
}
|
267
|
+
|
244
268
|
this.images.set(prompt.url, prompt.description);
|
245
269
|
const prefix = this.getPrefix();
|
246
270
|
const result = await stormClient.createImage(
|
@@ -258,25 +282,25 @@ export class PageQueue extends EventEmitter {
|
|
258
282
|
}
|
259
283
|
|
260
284
|
public async generate(prompt: UIPagePrompt, conversationId: string) {
|
261
|
-
const promises: Promise<void>[] = [];
|
262
285
|
const screenStream = await stormClient.createUIPage(prompt, conversationId);
|
263
|
-
|
286
|
+
let pageEvent: StormEventPage | null = null;
|
264
287
|
screenStream.on('data', (event: StormEvent) => {
|
265
288
|
if (event.type === 'PAGE') {
|
266
289
|
event.payload.conversationId = conversationId;
|
267
|
-
|
268
|
-
promises.push(this.processPageEventWithReferences(event));
|
290
|
+
pageEvent = event;
|
269
291
|
return;
|
270
292
|
}
|
271
|
-
|
272
293
|
this.emit('event', event);
|
273
294
|
});
|
274
295
|
|
275
296
|
await screenStream.waitForDone();
|
276
|
-
|
297
|
+
if (!pageEvent) {
|
298
|
+
throw new Error('No page was generated');
|
299
|
+
}
|
300
|
+
await this.processPageEventWithReferences(pageEvent);
|
277
301
|
}
|
278
302
|
|
279
|
-
private async resolveReferences(content: string) {
|
303
|
+
private async resolveReferences(content: string): Promise<ReferenceClassification[]> {
|
280
304
|
const referenceStream = await stormClient.classifyUIReferences(content);
|
281
305
|
|
282
306
|
const references: ReferenceClassification[] = [];
|
package/src/storm/routes.ts
CHANGED
@@ -168,6 +168,11 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
168
168
|
}
|
169
169
|
});
|
170
170
|
|
171
|
+
queue.on('error', (err) => {
|
172
|
+
console.error('Failed to process page', err);
|
173
|
+
sendError(err as any, res);
|
174
|
+
});
|
175
|
+
|
171
176
|
await queue.addPrompt(aiRequest, conversationId, true);
|
172
177
|
|
173
178
|
await queue.wait();
|
@@ -274,6 +279,11 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
274
279
|
sendEvent(res, screenData);
|
275
280
|
});
|
276
281
|
|
282
|
+
pageQueue.on('error', (err) => {
|
283
|
+
console.error('Failed to process page', err);
|
284
|
+
sendError(err as any, res);
|
285
|
+
});
|
286
|
+
|
277
287
|
await waitForStormStream(landingPagesStream);
|
278
288
|
await pageQueue.wait();
|
279
289
|
await Promise.allSettled(pageEventPromises);
|
@@ -464,6 +474,11 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
464
474
|
sendEvent(res, screenData);
|
465
475
|
});
|
466
476
|
|
477
|
+
queue.on('error', (err) => {
|
478
|
+
console.error('Failed to process page', err);
|
479
|
+
sendError(err as any, res);
|
480
|
+
});
|
481
|
+
|
467
482
|
for (const screen of Object.values(uniqueUserJourneyScreens)) {
|
468
483
|
queue
|
469
484
|
.addPrompt({
|
@@ -528,6 +543,11 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
|
|
528
543
|
}
|
529
544
|
});
|
530
545
|
|
546
|
+
queue.on('error', (err) => {
|
547
|
+
console.error('Failed to process page', err);
|
548
|
+
sendError(err as any, res);
|
549
|
+
});
|
550
|
+
|
531
551
|
const pages = aiRequest.prompt.pages.filter((page) => page.conversationId);
|
532
552
|
if (pages.length === 0) {
|
533
553
|
console.log('No pages to update', aiRequest.prompt.pages);
|
package/src/storm/stormClient.ts
CHANGED
@@ -16,6 +16,23 @@ import {
|
|
16
16
|
StormUIListPrompt,
|
17
17
|
} from './stream';
|
18
18
|
import { Page, StormEventPageUrl } from './events';
|
19
|
+
import { fetch, RequestInit, Agent, RetryAgent } from 'undici';
|
20
|
+
|
21
|
+
// Will only retry on error codes and GET requests by default
|
22
|
+
// See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
|
23
|
+
const retryAgent = new RetryAgent(new Agent(), {
|
24
|
+
methods: [
|
25
|
+
// Added methods ↓ (not idempotent), but we want to retry on POST:
|
26
|
+
'POST',
|
27
|
+
// defaults below
|
28
|
+
'GET',
|
29
|
+
'HEAD',
|
30
|
+
'OPTIONS',
|
31
|
+
'PUT',
|
32
|
+
'DELETE',
|
33
|
+
'TRACE',
|
34
|
+
],
|
35
|
+
});
|
19
36
|
|
20
37
|
export const STORM_ID = 'storm';
|
21
38
|
|
@@ -112,6 +129,7 @@ class StormClient {
|
|
112
129
|
method: method,
|
113
130
|
body: JSON.stringify(body),
|
114
131
|
headers,
|
132
|
+
dispatcher: retryAgent,
|
115
133
|
};
|
116
134
|
}
|
117
135
|
|
@@ -222,10 +240,7 @@ class StormClient {
|
|
222
240
|
conversationId,
|
223
241
|
});
|
224
242
|
|
225
|
-
return fetch(options.url,
|
226
|
-
method: options.method,
|
227
|
-
headers: options.headers,
|
228
|
-
});
|
243
|
+
return fetch(options.url, options);
|
229
244
|
}
|
230
245
|
|
231
246
|
public async getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string) {
|
@@ -234,10 +249,7 @@ class StormClient {
|
|
234
249
|
conversationId,
|
235
250
|
});
|
236
251
|
|
237
|
-
const response = await fetch(options.url,
|
238
|
-
method: options.method,
|
239
|
-
headers: options.headers,
|
240
|
-
});
|
252
|
+
const response = await fetch(options.url, options);
|
241
253
|
|
242
254
|
return response.json() as Promise<{ vote: -1 | 0 | 1 }>;
|
243
255
|
}
|
@@ -331,10 +343,7 @@ class StormClient {
|
|
331
343
|
conversationId: conversationId,
|
332
344
|
});
|
333
345
|
|
334
|
-
const response = await fetch(options.url,
|
335
|
-
method: options.method,
|
336
|
-
headers: options.headers,
|
337
|
-
});
|
346
|
+
const response = await fetch(options.url, options);
|
338
347
|
|
339
348
|
return response.text();
|
340
349
|
}
|