@kapeta/local-cluster-service 0.62.0 → 0.62.2

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,17 @@
1
+ ## [0.62.2](https://github.com/kapetacom/local-cluster-service/compare/v0.62.1...v0.62.2) (2024-08-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Single page adjustments ([#217](https://github.com/kapetacom/local-cluster-service/issues/217)) ([8d0f7e6](https://github.com/kapetacom/local-cluster-service/commit/8d0f7e6b9a3fdd971559a4c9b06b78db38145b4d))
7
+
8
+ ## [0.62.1](https://github.com/kapetacom/local-cluster-service/compare/v0.62.0...v0.62.1) (2024-08-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add openapi spec streaming for a sense of progress ([95eff45](https://github.com/kapetacom/local-cluster-service/commit/95eff4504fd83a2440f4343f608383299fa50326))
14
+
1
15
  # [0.62.0](https://github.com/kapetacom/local-cluster-service/compare/v0.61.2...v0.62.0) (2024-08-13)
2
16
 
3
17
 
@@ -4,18 +4,20 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage } from './events';
7
+ import { ReferenceClassification, StormEvent, StormEventPage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export declare class PageQueue extends EventEmitter {
10
10
  private readonly queue;
11
11
  private readonly systemId;
12
12
  private readonly references;
13
+ private uiShells;
13
14
  constructor(systemId: string, concurrency?: number);
14
15
  on(event: 'event', listener: (data: StormEvent) => void): this;
15
16
  on(event: 'page', listener: (data: StormEventPage) => void): this;
16
17
  emit(type: 'event', event: StormEvent): boolean;
17
18
  emit(type: 'page', event: StormEventPage): boolean;
18
- addPrompt(initialPrompt: UIPagePrompt): Promise<void>;
19
+ addUiShell(uiShell: UIShell): void;
20
+ addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
19
21
  private addPageGenerator;
20
22
  cancel(): void;
21
23
  wait(): Promise<void>;
@@ -12,10 +12,12 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
12
12
  const stormClient_1 = require("./stormClient");
13
13
  const node_events_1 = require("node:events");
14
14
  const PromiseQueue_1 = require("./PromiseQueue");
15
+ const page_utils_1 = require("./page-utils");
15
16
  class PageQueue extends node_events_1.EventEmitter {
16
17
  queue;
17
18
  systemId;
18
19
  references = new Map();
20
+ uiShells = [];
19
21
  constructor(systemId, concurrency = 5) {
20
22
  super();
21
23
  this.systemId = systemId;
@@ -27,14 +29,24 @@ class PageQueue extends node_events_1.EventEmitter {
27
29
  emit(eventName, ...args) {
28
30
  return super.emit(eventName, ...args);
29
31
  }
30
- addPrompt(initialPrompt) {
31
- if (this.references.has(initialPrompt.path)) {
32
+ addUiShell(uiShell) {
33
+ this.uiShells.push(uiShell);
34
+ }
35
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
36
+ if (!overwrite && this.references.has(initialPrompt.path)) {
32
37
  console.log('Ignoring duplicate prompt', initialPrompt.path);
33
38
  return Promise.resolve();
34
39
  }
35
- console.log('processing prompt', initialPrompt.path);
36
- const generator = new PageGenerator(initialPrompt);
37
- this.references.set(initialPrompt.path, generator);
40
+ if (!overwrite && (0, page_utils_1.hasPageOnDisk)(this.systemId, initialPrompt.method, initialPrompt.path)) {
41
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
42
+ return Promise.resolve();
43
+ }
44
+ const prompt = {
45
+ ...initialPrompt,
46
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
47
+ };
48
+ const generator = new PageGenerator(prompt, conversationId);
49
+ this.references.set(prompt.path, generator);
38
50
  return this.addPageGenerator(generator);
39
51
  }
40
52
  async addPageGenerator(generator) {
@@ -236,6 +236,17 @@ class StormEventParser {
236
236
  this.blocks[evt.payload.blockName].models = [];
237
237
  }
238
238
  break;
239
+ case 'API_STREAM_CHUNK':
240
+ case 'API_STREAM_CHUNK_RESET':
241
+ case 'API_STREAM_DONE':
242
+ case 'API_STREAM_FAILED':
243
+ case 'API_STREAM_STATE':
244
+ case 'API_STREAM_START':
245
+ if ('blockName' in evt.payload) {
246
+ evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString();
247
+ evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef);
248
+ }
249
+ break;
239
250
  }
240
251
  return await this.toResult(handle);
241
252
  }
@@ -218,6 +218,10 @@ export interface StormEventFileChunk extends StormEventFileBase {
218
218
  lineNumber: number;
219
219
  };
220
220
  }
221
+ export interface StormEventApiBase {
222
+ type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
223
+ payload: StormEventFileBasePayload;
224
+ }
221
225
  export interface StormEventBlockReady {
222
226
  type: 'BLOCK_READY';
223
227
  reason: string;
@@ -371,5 +375,5 @@ export interface StormEventReferenceClassification {
371
375
  created: number;
372
376
  payload: ReferenceClassification;
373
377
  }
374
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove | StormEventLandingPage | StormEventReferenceClassification;
378
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase;
375
379
  export {};
@@ -9,6 +9,7 @@ export declare const SystemIdHeader = "System-Id";
9
9
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
10
10
  path: string;
11
11
  }>;
12
+ export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
12
13
  export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
13
14
  export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
14
15
  export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -26,6 +26,13 @@ async function writePageToDisk(systemId, event) {
26
26
  };
27
27
  }
28
28
  exports.writePageToDisk = writePageToDisk;
29
+ function hasPageOnDisk(systemId, method, path) {
30
+ const baseDir = getBaseDir(systemId);
31
+ const filePath = getFilePath(method);
32
+ const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
33
+ return fs_extra_1.default.existsSync(fullPath);
34
+ }
35
+ exports.hasPageOnDisk = hasPageOnDisk;
29
36
  function getBaseDir(systemId) {
30
37
  return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
31
38
  }
@@ -71,19 +71,13 @@ router.post('/ui/screen', async (req, res) => {
71
71
  onRequestAborted(req, res, () => {
72
72
  queue.cancel();
73
73
  });
74
- await queue.addPrompt(aiRequest);
75
74
  const promises = [];
76
75
  queue.on('page', (data) => {
77
- switch (data.type) {
78
- case 'PAGE':
79
- console.log('Processing page event', data);
80
- if (systemId) {
81
- promises.push(sendPageEvent(systemId, data, res));
82
- }
83
- break;
76
+ if (systemId) {
77
+ promises.push(sendPageEvent(systemId, data, res));
84
78
  }
85
- sendEvent(res, data);
86
79
  });
80
+ await queue.addPrompt(aiRequest, conversationId, true);
87
81
  await queue.wait();
88
82
  await Promise.allSettled(promises);
89
83
  sendDone(res);
@@ -233,7 +227,7 @@ router.post('/:handle/ui', async (req, res) => {
233
227
  onRequestAborted(req, res, () => {
234
228
  shellsStream.abort();
235
229
  });
236
- const uiShells = [];
230
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
237
231
  shellsStream.on('data', (data) => {
238
232
  console.log('Processing shell event', data);
239
233
  sendEvent(res, data);
@@ -243,7 +237,7 @@ router.post('/:handle/ui', async (req, res) => {
243
237
  if (shellsStream.isAborted()) {
244
238
  return;
245
239
  }
246
- uiShells.push(data.payload);
240
+ queue.addUiShell(data.payload);
247
241
  });
248
242
  shellsStream.on('error', (error) => {
249
243
  console.error('Error on shellsStream', error);
@@ -254,7 +248,6 @@ router.post('/:handle/ui', async (req, res) => {
254
248
  UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
255
249
  await UI_SERVERS[outerConversationId].start();
256
250
  // Get the pages (5 at a time)
257
- const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
258
251
  const pagePromises = [];
259
252
  onRequestAborted(req, res, () => {
260
253
  queue.cancel();
@@ -276,7 +269,6 @@ router.post('/:handle/ui', async (req, res) => {
276
269
  title: screen.title,
277
270
  filename: screen.filename,
278
271
  storage_prefix: outerConversationId + '_',
279
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
280
272
  }));
281
273
  }
282
274
  await queue.wait();
@@ -377,6 +369,7 @@ router.post('/:handle/all', async (req, res) => {
377
369
  try {
378
370
  const result = await eventParser.processEvent(handle, data);
379
371
  switch (data.type) {
372
+ case 'API_STREAM_START':
380
373
  case 'CREATE_API':
381
374
  case 'CREATE_MODEL':
382
375
  case 'CREATE_TYPE':
@@ -535,7 +528,7 @@ function streamStormPartialResponse(result, res) {
535
528
  switch (data.type) {
536
529
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
537
530
  case 'ERROR_INTERNAL':
538
- console.log("Error internal", data);
531
+ console.log('Error internal', data);
539
532
  return;
540
533
  }
541
534
  sendEvent(res, data);
@@ -554,13 +547,13 @@ function onRequestAborted(req, res, onAborted) {
554
547
  onAborted();
555
548
  });
556
549
  }
557
- function sendPageEvent(mainConversationId, data, res) {
558
- return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
559
- .catch((err) => {
550
+ async function sendPageEvent(mainConversationId, data, res) {
551
+ try {
552
+ await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
553
+ }
554
+ catch (err) {
560
555
  console.error('Failed to write page to disk', err);
561
- })
562
- .then(() => {
563
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
564
- });
556
+ }
557
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
565
558
  }
566
559
  exports.default = router;
@@ -4,18 +4,20 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage } from './events';
7
+ import { ReferenceClassification, StormEvent, StormEventPage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export declare class PageQueue extends EventEmitter {
10
10
  private readonly queue;
11
11
  private readonly systemId;
12
12
  private readonly references;
13
+ private uiShells;
13
14
  constructor(systemId: string, concurrency?: number);
14
15
  on(event: 'event', listener: (data: StormEvent) => void): this;
15
16
  on(event: 'page', listener: (data: StormEventPage) => void): this;
16
17
  emit(type: 'event', event: StormEvent): boolean;
17
18
  emit(type: 'page', event: StormEventPage): boolean;
18
- addPrompt(initialPrompt: UIPagePrompt): Promise<void>;
19
+ addUiShell(uiShell: UIShell): void;
20
+ addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
19
21
  private addPageGenerator;
20
22
  cancel(): void;
21
23
  wait(): Promise<void>;
@@ -12,10 +12,12 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
12
12
  const stormClient_1 = require("./stormClient");
13
13
  const node_events_1 = require("node:events");
14
14
  const PromiseQueue_1 = require("./PromiseQueue");
15
+ const page_utils_1 = require("./page-utils");
15
16
  class PageQueue extends node_events_1.EventEmitter {
16
17
  queue;
17
18
  systemId;
18
19
  references = new Map();
20
+ uiShells = [];
19
21
  constructor(systemId, concurrency = 5) {
20
22
  super();
21
23
  this.systemId = systemId;
@@ -27,14 +29,24 @@ class PageQueue extends node_events_1.EventEmitter {
27
29
  emit(eventName, ...args) {
28
30
  return super.emit(eventName, ...args);
29
31
  }
30
- addPrompt(initialPrompt) {
31
- if (this.references.has(initialPrompt.path)) {
32
+ addUiShell(uiShell) {
33
+ this.uiShells.push(uiShell);
34
+ }
35
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
36
+ if (!overwrite && this.references.has(initialPrompt.path)) {
32
37
  console.log('Ignoring duplicate prompt', initialPrompt.path);
33
38
  return Promise.resolve();
34
39
  }
35
- console.log('processing prompt', initialPrompt.path);
36
- const generator = new PageGenerator(initialPrompt);
37
- this.references.set(initialPrompt.path, generator);
40
+ if (!overwrite && (0, page_utils_1.hasPageOnDisk)(this.systemId, initialPrompt.method, initialPrompt.path)) {
41
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
42
+ return Promise.resolve();
43
+ }
44
+ const prompt = {
45
+ ...initialPrompt,
46
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
47
+ };
48
+ const generator = new PageGenerator(prompt, conversationId);
49
+ this.references.set(prompt.path, generator);
38
50
  return this.addPageGenerator(generator);
39
51
  }
40
52
  async addPageGenerator(generator) {
@@ -236,6 +236,17 @@ class StormEventParser {
236
236
  this.blocks[evt.payload.blockName].models = [];
237
237
  }
238
238
  break;
239
+ case 'API_STREAM_CHUNK':
240
+ case 'API_STREAM_CHUNK_RESET':
241
+ case 'API_STREAM_DONE':
242
+ case 'API_STREAM_FAILED':
243
+ case 'API_STREAM_STATE':
244
+ case 'API_STREAM_START':
245
+ if ('blockName' in evt.payload) {
246
+ evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString();
247
+ evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef);
248
+ }
249
+ break;
239
250
  }
240
251
  return await this.toResult(handle);
241
252
  }
@@ -218,6 +218,10 @@ export interface StormEventFileChunk extends StormEventFileBase {
218
218
  lineNumber: number;
219
219
  };
220
220
  }
221
+ export interface StormEventApiBase {
222
+ type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
223
+ payload: StormEventFileBasePayload;
224
+ }
221
225
  export interface StormEventBlockReady {
222
226
  type: 'BLOCK_READY';
223
227
  reason: string;
@@ -371,5 +375,5 @@ export interface StormEventReferenceClassification {
371
375
  created: number;
372
376
  payload: ReferenceClassification;
373
377
  }
374
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove | StormEventLandingPage | StormEventReferenceClassification;
378
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase;
375
379
  export {};
@@ -9,6 +9,7 @@ export declare const SystemIdHeader = "System-Id";
9
9
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
10
10
  path: string;
11
11
  }>;
12
+ export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
12
13
  export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
13
14
  export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
14
15
  export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -26,6 +26,13 @@ async function writePageToDisk(systemId, event) {
26
26
  };
27
27
  }
28
28
  exports.writePageToDisk = writePageToDisk;
29
+ function hasPageOnDisk(systemId, method, path) {
30
+ const baseDir = getBaseDir(systemId);
31
+ const filePath = getFilePath(method);
32
+ const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
33
+ return fs_extra_1.default.existsSync(fullPath);
34
+ }
35
+ exports.hasPageOnDisk = hasPageOnDisk;
29
36
  function getBaseDir(systemId) {
30
37
  return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
31
38
  }
@@ -71,19 +71,13 @@ router.post('/ui/screen', async (req, res) => {
71
71
  onRequestAborted(req, res, () => {
72
72
  queue.cancel();
73
73
  });
74
- await queue.addPrompt(aiRequest);
75
74
  const promises = [];
76
75
  queue.on('page', (data) => {
77
- switch (data.type) {
78
- case 'PAGE':
79
- console.log('Processing page event', data);
80
- if (systemId) {
81
- promises.push(sendPageEvent(systemId, data, res));
82
- }
83
- break;
76
+ if (systemId) {
77
+ promises.push(sendPageEvent(systemId, data, res));
84
78
  }
85
- sendEvent(res, data);
86
79
  });
80
+ await queue.addPrompt(aiRequest, conversationId, true);
87
81
  await queue.wait();
88
82
  await Promise.allSettled(promises);
89
83
  sendDone(res);
@@ -233,7 +227,7 @@ router.post('/:handle/ui', async (req, res) => {
233
227
  onRequestAborted(req, res, () => {
234
228
  shellsStream.abort();
235
229
  });
236
- const uiShells = [];
230
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
237
231
  shellsStream.on('data', (data) => {
238
232
  console.log('Processing shell event', data);
239
233
  sendEvent(res, data);
@@ -243,7 +237,7 @@ router.post('/:handle/ui', async (req, res) => {
243
237
  if (shellsStream.isAborted()) {
244
238
  return;
245
239
  }
246
- uiShells.push(data.payload);
240
+ queue.addUiShell(data.payload);
247
241
  });
248
242
  shellsStream.on('error', (error) => {
249
243
  console.error('Error on shellsStream', error);
@@ -254,7 +248,6 @@ router.post('/:handle/ui', async (req, res) => {
254
248
  UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
255
249
  await UI_SERVERS[outerConversationId].start();
256
250
  // Get the pages (5 at a time)
257
- const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
258
251
  const pagePromises = [];
259
252
  onRequestAborted(req, res, () => {
260
253
  queue.cancel();
@@ -276,7 +269,6 @@ router.post('/:handle/ui', async (req, res) => {
276
269
  title: screen.title,
277
270
  filename: screen.filename,
278
271
  storage_prefix: outerConversationId + '_',
279
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
280
272
  }));
281
273
  }
282
274
  await queue.wait();
@@ -377,6 +369,7 @@ router.post('/:handle/all', async (req, res) => {
377
369
  try {
378
370
  const result = await eventParser.processEvent(handle, data);
379
371
  switch (data.type) {
372
+ case 'API_STREAM_START':
380
373
  case 'CREATE_API':
381
374
  case 'CREATE_MODEL':
382
375
  case 'CREATE_TYPE':
@@ -535,7 +528,7 @@ function streamStormPartialResponse(result, res) {
535
528
  switch (data.type) {
536
529
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
537
530
  case 'ERROR_INTERNAL':
538
- console.log("Error internal", data);
531
+ console.log('Error internal', data);
539
532
  return;
540
533
  }
541
534
  sendEvent(res, data);
@@ -554,13 +547,13 @@ function onRequestAborted(req, res, onAborted) {
554
547
  onAborted();
555
548
  });
556
549
  }
557
- function sendPageEvent(mainConversationId, data, res) {
558
- return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
559
- .catch((err) => {
550
+ async function sendPageEvent(mainConversationId, data, res) {
551
+ try {
552
+ await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
553
+ }
554
+ catch (err) {
560
555
  console.error('Failed to write page to disk', err);
561
- })
562
- .then(() => {
563
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
564
- });
556
+ }
557
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
565
558
  }
566
559
  exports.default = router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.62.0",
3
+ "version": "0.62.2",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -5,14 +5,22 @@
5
5
 
6
6
  import uuid from 'node-uuid';
7
7
  import { stormClient, UIPagePrompt } from './stormClient';
8
- import { ReferenceClassification, StormEvent, StormEventPage, StormEventReferenceClassification } from './events';
8
+ import {
9
+ ReferenceClassification,
10
+ StormEvent,
11
+ StormEventPage,
12
+ StormEventReferenceClassification,
13
+ UIShell,
14
+ } from './events';
9
15
  import { EventEmitter } from 'node:events';
10
16
  import { PromiseQueue } from './PromiseQueue';
17
+ import { hasPageOnDisk } from './page-utils';
11
18
 
12
19
  export class PageQueue extends EventEmitter {
13
20
  private readonly queue: PromiseQueue;
14
21
  private readonly systemId: string;
15
22
  private readonly references: Map<string, PageGenerator> = new Map();
23
+ private uiShells: UIShell[] = [];
16
24
 
17
25
  constructor(systemId: string, concurrency: number = 5) {
18
26
  super();
@@ -33,14 +41,33 @@ export class PageQueue extends EventEmitter {
33
41
  return super.emit(eventName, ...args);
34
42
  }
35
43
 
36
- public addPrompt(initialPrompt: UIPagePrompt) {
37
- if (this.references.has(initialPrompt.path)) {
44
+ public addUiShell(uiShell: UIShell) {
45
+ this.uiShells.push(uiShell);
46
+ }
47
+
48
+ public addPrompt(
49
+ initialPrompt: Omit<UIPagePrompt, 'shell_page'>,
50
+ conversationId: string = uuid.v4(),
51
+ overwrite: boolean = false
52
+ ) {
53
+ if (!overwrite && this.references.has(initialPrompt.path)) {
38
54
  console.log('Ignoring duplicate prompt', initialPrompt.path);
39
55
  return Promise.resolve();
40
56
  }
41
- console.log('processing prompt', initialPrompt.path);
42
- const generator = new PageGenerator(initialPrompt);
43
- this.references.set(initialPrompt.path, generator);
57
+
58
+ if (!overwrite && hasPageOnDisk(this.systemId, initialPrompt.method, initialPrompt.path)) {
59
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
60
+ return Promise.resolve();
61
+ }
62
+
63
+ const prompt: UIPagePrompt = {
64
+ ...initialPrompt,
65
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
66
+ };
67
+
68
+ const generator = new PageGenerator(prompt, conversationId);
69
+ this.references.set(prompt.path, generator);
70
+
44
71
  return this.addPageGenerator(generator);
45
72
  }
46
73
 
@@ -357,6 +357,18 @@ export class StormEventParser {
357
357
  this.blocks[evt.payload.blockName].models = [];
358
358
  }
359
359
  break;
360
+
361
+ case 'API_STREAM_CHUNK':
362
+ case 'API_STREAM_CHUNK_RESET':
363
+ case 'API_STREAM_DONE':
364
+ case 'API_STREAM_FAILED':
365
+ case 'API_STREAM_STATE':
366
+ case 'API_STREAM_START':
367
+ if ('blockName' in evt.payload) {
368
+ evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString();
369
+ evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef);
370
+ }
371
+ break;
360
372
  }
361
373
 
362
374
  return await this.toResult(handle);
@@ -263,6 +263,11 @@ export interface StormEventFileChunk extends StormEventFileBase {
263
263
  };
264
264
  }
265
265
 
266
+ export interface StormEventApiBase {
267
+ type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
268
+ payload: StormEventFileBasePayload;
269
+ }
270
+
266
271
  export interface StormEventBlockReady {
267
272
  type: 'BLOCK_READY';
268
273
  reason: string;
@@ -471,4 +476,5 @@ export type StormEvent =
471
476
  | StormEventPageUrl
472
477
  | StormEventPromptImprove
473
478
  | StormEventLandingPage
474
- | StormEventReferenceClassification;
479
+ | StormEventReferenceClassification
480
+ | StormEventApiBase;
@@ -34,6 +34,13 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
34
34
  };
35
35
  }
36
36
 
37
+ export function hasPageOnDisk(systemId: string, method: string, path: string) {
38
+ const baseDir = getBaseDir(systemId);
39
+ const filePath = getFilePath(method);
40
+ const fullPath = Path.join(baseDir, normalizePath(path), filePath);
41
+ return FS.existsSync(fullPath);
42
+ }
43
+
37
44
  function getBaseDir(systemId: string) {
38
45
  return Path.join(os.tmpdir(), 'ai-systems', systemId);
39
46
  }
@@ -106,22 +106,16 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
106
106
  queue.cancel();
107
107
  });
108
108
 
109
- await queue.addPrompt(aiRequest);
110
-
111
109
  const promises: Promise<void>[] = [];
112
- queue.on('page', (data) => {
113
- switch (data.type) {
114
- case 'PAGE':
115
- console.log('Processing page event', data);
116
110
 
117
- if (systemId) {
118
- promises.push(sendPageEvent(systemId, data, res));
119
- }
120
- break;
111
+ queue.on('page', (data) => {
112
+ if (systemId) {
113
+ promises.push(sendPageEvent(systemId, data, res));
121
114
  }
122
- sendEvent(res, data);
123
115
  });
124
116
 
117
+ await queue.addPrompt(aiRequest, conversationId, true);
118
+
125
119
  await queue.wait();
126
120
  await Promise.allSettled(promises);
127
121
 
@@ -304,8 +298,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
304
298
  shellsStream.abort();
305
299
  });
306
300
 
307
- const uiShells: UIShell[] = [];
308
-
301
+ const queue = new PageQueue(outerConversationId, 5);
309
302
  shellsStream.on('data', (data: StormEvent) => {
310
303
  console.log('Processing shell event', data);
311
304
  sendEvent(res, data);
@@ -318,7 +311,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
318
311
  return;
319
312
  }
320
313
 
321
- uiShells.push(data.payload);
314
+ queue.addUiShell(data.payload);
322
315
  });
323
316
 
324
317
  shellsStream.on('error', (error) => {
@@ -333,7 +326,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
333
326
  await UI_SERVERS[outerConversationId].start();
334
327
 
335
328
  // Get the pages (5 at a time)
336
- const queue = new PageQueue(outerConversationId, 5);
329
+
337
330
  const pagePromises: Promise<void>[] = [];
338
331
  onRequestAborted(req, res, () => {
339
332
  queue.cancel();
@@ -359,7 +352,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
359
352
  title: screen.title,
360
353
  filename: screen.filename,
361
354
  storage_prefix: outerConversationId + '_',
362
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
363
355
  })
364
356
  );
365
357
  }
@@ -483,6 +475,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
483
475
  const result = await eventParser.processEvent(handle, data);
484
476
 
485
477
  switch (data.type) {
478
+ case 'API_STREAM_START':
486
479
  case 'CREATE_API':
487
480
  case 'CREATE_MODEL':
488
481
  case 'CREATE_TYPE':
@@ -667,7 +660,7 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
667
660
  switch (data.type) {
668
661
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
669
662
  case 'ERROR_INTERNAL':
670
- console.log("Error internal", data);
663
+ console.log('Error internal', data);
671
664
  return;
672
665
  }
673
666
  sendEvent(res, data);
@@ -690,14 +683,13 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
690
683
  });
691
684
  }
692
685
 
693
- function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
694
- return writePageToDisk(mainConversationId, data)
695
- .catch((err) => {
696
- console.error('Failed to write page to disk', err);
697
- })
698
- .then(() => {
699
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
700
- });
686
+ async function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
687
+ try {
688
+ await writePageToDisk(mainConversationId, data);
689
+ } catch (err) {
690
+ console.error('Failed to write page to disk', err);
691
+ }
692
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
701
693
  }
702
694
 
703
695
  export default router;