@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 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
- void this.eventQueue.add(async () => listener(...args));
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
- void initialPrompts.map((prompt) => {
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
- return this.addPrompt(prompt);
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
- promises.push(this.processPageEventWithReferences(event));
260
+ pageEvent = event;
217
261
  return;
218
262
  }
219
263
  this.emit('event', event);
220
264
  });
221
265
  await screenStream.waitForDone();
222
- await Promise.all(promises);
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
- void this.eventQueue.add(async () => listener(...args));
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
- void initialPrompts.map((prompt) => {
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
- return this.addPrompt(prompt);
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
- promises.push(this.processPageEventWithReferences(event));
260
+ pageEvent = event;
217
261
  return;
218
262
  }
219
263
  this.emit('event', event);
220
264
  });
221
265
  await screenStream.waitForDone();
222
- await Promise.all(promises);
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.5",
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
- void this.eventQueue.add(async () => listener(...args));
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
- void initialPrompts.map((prompt) => {
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
- return this.addPrompt(prompt);
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
- await Promise.all(promises);
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[] = [];
@@ -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);
@@ -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
  }