@kapeta/local-cluster-service 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.51.0](https://github.com/kapetacom/local-cluster-service/compare/v0.50.0...v0.51.0) (2024-06-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Handle file streaming events ([#164](https://github.com/kapetacom/local-cluster-service/issues/164)) ([441847a](https://github.com/kapetacom/local-cluster-service/commit/441847a12d20826d843e62627ea9bf584043effb))
7
+
8
+ # [0.50.0](https://github.com/kapetacom/local-cluster-service/compare/v0.49.0...v0.50.0) (2024-06-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * Handle aborted requests ([#162](https://github.com/kapetacom/local-cluster-service/issues/162)) ([a9323d4](https://github.com/kapetacom/local-cluster-service/commit/a9323d46423361c2de63e40b4b61927b9b4198b7))
14
+
1
15
  # [0.49.0](https://github.com/kapetacom/local-cluster-service/compare/v0.48.5...v0.49.0) (2024-06-05)
2
16
 
3
17
 
@@ -14,10 +14,12 @@ export declare class StormCodegen {
14
14
  private readonly conversationId;
15
15
  constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
16
16
  process(): Promise<void>;
17
+ isAborted(): boolean;
17
18
  getStream(): StormStream;
18
19
  private handleTemplateFileOutput;
19
20
  private handleUiOutput;
20
- private handleFileOutput;
21
+ private handleFileEvents;
22
+ private handleFileDoneOutput;
21
23
  private getBasePath;
22
24
  /**
23
25
  * Generates the code for a block and sends it to the AI
@@ -33,7 +35,7 @@ export declare class StormCodegen {
33
35
  /**
34
36
  * Emits the text-based files to the stream
35
37
  */
36
- private emitFiles;
38
+ private emitStaticFiles;
37
39
  private emitFile;
38
40
  /**
39
41
  * Sends the template to the AI and processes the response
@@ -47,4 +49,5 @@ export declare class StormCodegen {
47
49
  * Generates the code using codegen for a given block.
48
50
  */
49
51
  private generateBlock;
52
+ abort(): void;
50
53
  }
@@ -42,6 +42,49 @@ const path_1 = __importStar(require("path"));
42
42
  const node_os_1 = __importDefault(require("node:os"));
43
43
  const fs_1 = require("fs");
44
44
  const path_2 = __importDefault(require("path"));
45
+ const SIMULATED_DELAY = 1000;
46
+ class SimulatedFileDelay {
47
+ file;
48
+ stream;
49
+ constructor(file, stream) {
50
+ this.file = file;
51
+ this.stream = stream;
52
+ }
53
+ async start() {
54
+ const commonPayload = {
55
+ filename: this.file.payload.filename,
56
+ path: this.file.payload.path,
57
+ blockName: this.file.payload.blockName,
58
+ blockRef: this.file.payload.blockRef,
59
+ instanceId: this.file.payload.instanceId,
60
+ };
61
+ this.stream.emit('data', {
62
+ type: 'FILE_START',
63
+ created: Date.now(),
64
+ reason: 'File start',
65
+ payload: commonPayload,
66
+ });
67
+ const lines = this.file.payload.content.split('\n');
68
+ const delayPerLine = SIMULATED_DELAY / lines.length;
69
+ for (const line of lines) {
70
+ await new Promise((resolve) => {
71
+ setTimeout(() => {
72
+ this.stream.emit('data', {
73
+ type: 'FILE_CHUNK',
74
+ created: Date.now(),
75
+ reason: 'File chunk',
76
+ payload: {
77
+ ...commonPayload,
78
+ content: line,
79
+ },
80
+ });
81
+ resolve();
82
+ }, delayPerLine);
83
+ });
84
+ }
85
+ this.stream.emit('data', this.file);
86
+ }
87
+ }
45
88
  class StormCodegen {
46
89
  userPrompt;
47
90
  blocks;
@@ -63,46 +106,139 @@ class StormCodegen {
63
106
  await Promise.all(promises);
64
107
  this.out.end();
65
108
  }
109
+ isAborted() {
110
+ return this.out.isAborted();
111
+ }
66
112
  getStream() {
67
113
  return this.out;
68
114
  }
69
115
  handleTemplateFileOutput(blockUri, aiName, template, data) {
116
+ if (this.handleFileEvents(blockUri, aiName, data)) {
117
+ return;
118
+ }
70
119
  switch (data.type) {
71
- case 'FILE':
120
+ case 'FILE_DONE':
72
121
  template.filename = data.payload.filename;
73
122
  template.content = data.payload.content;
74
- return this.handleFileOutput(blockUri, aiName, data);
123
+ this.handleFileDoneOutput(blockUri, aiName, data);
124
+ break;
75
125
  }
76
126
  }
77
- handleUiOutput(blockUri, aiName, data) {
127
+ handleUiOutput(blockUri, blockName, data) {
128
+ const blockRef = blockUri.toNormalizedString();
129
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
130
+ if (this.handleFileEvents(blockUri, blockName, data)) {
131
+ return;
132
+ }
78
133
  switch (data.type) {
79
134
  case 'SCREEN':
80
- const ref = blockUri.toNormalizedString();
81
135
  this.out.emit('data', {
82
136
  type: 'SCREEN',
83
137
  reason: data.reason,
84
138
  created: Date.now(),
85
139
  payload: {
86
140
  ...data.payload,
87
- blockName: aiName,
88
- blockRef: ref,
89
- instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
141
+ blockName,
142
+ blockRef,
143
+ instanceId,
144
+ },
145
+ });
146
+ break;
147
+ case 'FILE_START':
148
+ case 'FILE_CHUNK_RESET':
149
+ this.out.emit('data', {
150
+ ...data,
151
+ payload: {
152
+ ...data.payload,
153
+ blockName,
154
+ blockRef,
155
+ instanceId,
156
+ },
157
+ });
158
+ break;
159
+ case 'FILE_CHUNK':
160
+ this.out.emit('data', {
161
+ ...data,
162
+ payload: {
163
+ ...data.payload,
164
+ blockName,
165
+ blockRef,
166
+ instanceId,
90
167
  },
91
168
  });
92
- case 'FILE':
93
- return this.handleFileOutput(blockUri, aiName, data);
169
+ break;
170
+ case 'FILE_STATE':
171
+ this.out.emit('data', {
172
+ ...data,
173
+ payload: {
174
+ ...data.payload,
175
+ blockName,
176
+ blockRef,
177
+ instanceId,
178
+ },
179
+ });
180
+ break;
181
+ case 'FILE_DONE':
182
+ this.handleFileDoneOutput(blockUri, blockName, data);
183
+ break;
94
184
  }
95
185
  }
96
- handleFileOutput(blockUri, aiName, data) {
186
+ handleFileEvents(blockUri, blockName, data) {
187
+ const blockRef = blockUri.toNormalizedString();
188
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
189
+ const basePath = this.getBasePath(blockUri.fullName);
97
190
  switch (data.type) {
98
- case 'FILE':
191
+ case 'FILE_START':
192
+ case 'FILE_CHUNK_RESET':
193
+ this.out.emit('data', {
194
+ ...data,
195
+ payload: {
196
+ ...data.payload,
197
+ path: (0, path_1.join)(basePath, data.payload.filename),
198
+ blockName,
199
+ blockRef,
200
+ instanceId,
201
+ },
202
+ });
203
+ return true;
204
+ case 'FILE_CHUNK':
205
+ this.out.emit('data', {
206
+ ...data,
207
+ payload: {
208
+ ...data.payload,
209
+ path: (0, path_1.join)(basePath, data.payload.filename),
210
+ blockName,
211
+ blockRef,
212
+ instanceId,
213
+ },
214
+ });
215
+ return true;
216
+ case 'FILE_STATE':
217
+ this.out.emit('data', {
218
+ ...data,
219
+ payload: {
220
+ ...data.payload,
221
+ path: (0, path_1.join)(basePath, data.payload.filename),
222
+ blockName,
223
+ blockRef,
224
+ instanceId,
225
+ },
226
+ });
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ handleFileDoneOutput(blockUri, aiName, data) {
232
+ switch (data.type) {
233
+ case 'FILE_DONE':
99
234
  const ref = blockUri.toNormalizedString();
100
235
  this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
101
236
  return {
102
- type: 'FILE',
237
+ type: 'FILE_DONE',
103
238
  created: Date.now(),
104
239
  payload: {
105
240
  filename: data.payload.filename,
241
+ path: (0, path_1.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
106
242
  content: data.payload.content,
107
243
  blockRef: ref,
108
244
  instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
@@ -117,6 +253,9 @@ class StormCodegen {
117
253
  * Generates the code for a block and sends it to the AI
118
254
  */
119
255
  async processBlockCode(block) {
256
+ if (this.isAborted()) {
257
+ return;
258
+ }
120
259
  // Generate the code for the block using the standard codegen templates
121
260
  const generatedResult = await this.generateBlock(block.content);
122
261
  if (!generatedResult) {
@@ -124,7 +263,10 @@ class StormCodegen {
124
263
  }
125
264
  const allFiles = this.toStormFiles(generatedResult);
126
265
  // Send all the non-ai files to the stream
127
- this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
266
+ await this.emitStaticFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
267
+ if (this.isAborted()) {
268
+ return;
269
+ }
128
270
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
129
271
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
130
272
  if (uiTemplates.length > 0) {
@@ -138,8 +280,14 @@ class StormCodegen {
138
280
  uiStream.on('data', (evt) => {
139
281
  this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
140
282
  });
283
+ this.out.on('aborted', () => {
284
+ uiStream.abort();
285
+ });
141
286
  await uiStream.waitForDone();
142
287
  }
288
+ if (this.isAborted()) {
289
+ return;
290
+ }
143
291
  // Gather the context files for implementation. These will be all be passed to the AI
144
292
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
145
293
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
@@ -148,6 +296,9 @@ class StormCodegen {
148
296
  await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
149
297
  }
150
298
  const basePath = this.getBasePath(block.content.metadata.name);
299
+ if (this.isAborted()) {
300
+ return;
301
+ }
151
302
  for (const serviceFile of serviceFiles) {
152
303
  const filePath = (0, path_1.join)(basePath, serviceFile.filename);
153
304
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -192,6 +343,9 @@ class StormCodegen {
192
343
  console.debug('Validation error:', result);
193
344
  const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
194
345
  const fixes = new Map();
346
+ this.out.on('aborted', () => {
347
+ errorStream.abort();
348
+ });
195
349
  errorStream.on('data', (evt) => {
196
350
  if (evt.type === 'ERROR_CLASSIFIER') {
197
351
  // find the file that caused the error
@@ -206,8 +360,8 @@ class StormCodegen {
206
360
  const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
207
361
  .map((e) => e.filename)
208
362
  .join('\n')}\n---\n${content}`;
209
- console.log(`trying to fix the code in ${eventFileName}`);
210
- console.debug(`with the fix:\n${fix}`);
363
+ //console.log(`trying to fix the code in ${eventFileName}`);
364
+ //console.debug(`with the fix:\n${fix}`);
211
365
  const code = this.codeFix(fix);
212
366
  fixes.set((0, path_1.join)(basePath, eventFileName), code);
213
367
  }
@@ -252,6 +406,9 @@ class StormCodegen {
252
406
  resolve(evt.payload.content);
253
407
  }
254
408
  });
409
+ this.out.on('aborted', () => {
410
+ fixStream.abort();
411
+ });
255
412
  fixStream.on('error', (err) => {
256
413
  reject(err);
257
414
  });
@@ -261,8 +418,8 @@ class StormCodegen {
261
418
  /**
262
419
  * Emits the text-based files to the stream
263
420
  */
264
- emitFiles(uri, aiName, files) {
265
- files.forEach((file) => {
421
+ async emitStaticFiles(uri, aiName, files) {
422
+ const promises = files.map((file) => {
266
423
  if (!file.content || typeof file.content !== 'string') {
267
424
  return;
268
425
  }
@@ -276,14 +433,30 @@ class StormCodegen {
276
433
  // They will need to be implemented by the AI
277
434
  return;
278
435
  }
279
- this.emitFile(uri, aiName, file.filename, file.content);
436
+ const basePath = this.getBasePath(uri.fullName);
437
+ const ref = uri.toNormalizedString();
438
+ const fileEvent = {
439
+ type: 'FILE_DONE',
440
+ reason: 'File generated',
441
+ created: Date.now(),
442
+ payload: {
443
+ filename: file.filename,
444
+ path: (0, path_1.join)(basePath, file.filename),
445
+ content: file.content,
446
+ blockName: aiName,
447
+ blockRef: ref,
448
+ instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
449
+ },
450
+ };
451
+ return new SimulatedFileDelay(fileEvent, this.out).start();
280
452
  });
453
+ return Promise.all(promises);
281
454
  }
282
455
  emitFile(uri, blockName, filename, content, reason = 'File generated') {
283
456
  const basePath = this.getBasePath(uri.fullName);
284
457
  const ref = uri.toNormalizedString();
285
458
  this.out.emit('data', {
286
- type: 'FILE',
459
+ type: 'FILE_DONE',
287
460
  reason,
288
461
  created: Date.now(),
289
462
  payload: {
@@ -306,18 +479,15 @@ class StormCodegen {
306
479
  template: templateFile,
307
480
  prompt: this.userPrompt,
308
481
  });
309
- const files = [];
482
+ this.out.on('aborted', () => {
483
+ stream.abort();
484
+ });
310
485
  stream.on('data', (evt) => {
311
- const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
312
- if (file) {
313
- files.push(file);
314
- }
486
+ this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
315
487
  });
316
488
  await stream.waitForDone();
317
- return files;
318
489
  });
319
- const fileChunks = await Promise.all(promises);
320
- return fileChunks.flat();
490
+ await Promise.all(promises);
321
491
  }
322
492
  /**
323
493
  * Converts the generated files to a format that can be sent to the AI
@@ -357,6 +527,9 @@ class StormCodegen {
357
527
  * Generates the code using codegen for a given block.
358
528
  */
359
529
  async generateBlock(yamlContent) {
530
+ if (this.isAborted()) {
531
+ return;
532
+ }
360
533
  if (!yamlContent.spec.target?.kind) {
361
534
  //Not all block types have targets
362
535
  return;
@@ -371,5 +544,8 @@ class StormCodegen {
371
544
  new codegen_1.CodeWriter(basePath).write(generatedResult);
372
545
  return generatedResult;
373
546
  }
547
+ abort() {
548
+ this.out.abort();
549
+ }
374
550
  }
375
551
  exports.StormCodegen = StormCodegen;
@@ -2,7 +2,7 @@
2
2
  * Copyright 2023 Kapeta Inc.
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
- import { StormEvent } from './events';
5
+ import { StormEvent, StormEventPhases, StormEventPhaseType } from './events';
6
6
  import { BlockDefinition, Plan } from '@kapeta/schemas';
7
7
  import { KapetaURI } from '@kapeta/nodejs-utils';
8
8
  export interface BlockDefinitionInfo {
@@ -39,6 +39,9 @@ export interface StormOptions {
39
39
  desktopLanguage: string;
40
40
  gatewayKind: string;
41
41
  }
42
+ export declare function createPhaseStartEvent(type: StormEventPhaseType): StormEventPhases;
43
+ export declare function createPhaseEndEvent(type: StormEventPhaseType): StormEventPhases;
44
+ export declare function createPhaseEvent(start: boolean, type: StormEventPhaseType): StormEventPhases;
42
45
  export declare function resolveOptions(): Promise<StormOptions>;
43
46
  export declare class StormEventParser {
44
47
  static toInstanceId(handle: string, blockName: string): string;
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: BUSL-1.1
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.StormEventParser = exports.resolveOptions = void 0;
7
+ exports.StormEventParser = exports.resolveOptions = exports.createPhaseEvent = exports.createPhaseEndEvent = exports.createPhaseStartEvent = void 0;
8
8
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
9
9
  const kaplang_core_1 = require("@kapeta/kaplang-core");
10
10
  const uuid_1 = require("uuid");
@@ -29,6 +29,24 @@ function prettifyKaplang(source) {
29
29
  return source;
30
30
  }
31
31
  }
32
+ function createPhaseStartEvent(type) {
33
+ return createPhaseEvent(true, type);
34
+ }
35
+ exports.createPhaseStartEvent = createPhaseStartEvent;
36
+ function createPhaseEndEvent(type) {
37
+ return createPhaseEvent(false, type);
38
+ }
39
+ exports.createPhaseEndEvent = createPhaseEndEvent;
40
+ function createPhaseEvent(start, type) {
41
+ return {
42
+ type: start ? 'PHASE_START' : 'PHASE_END',
43
+ created: Date.now(),
44
+ payload: {
45
+ phaseType: type,
46
+ },
47
+ };
48
+ }
49
+ exports.createPhaseEvent = createPhaseEvent;
32
50
  async function resolveOptions() {
33
51
  // Predefined types for now - TODO: Allow user to select / change
34
52
  const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
@@ -193,10 +211,6 @@ class StormEventParser {
193
211
  evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent);
194
212
  this.connections.push(evt.payload);
195
213
  break;
196
- default:
197
- case 'SCREEN_CANDIDATE':
198
- case 'FILE':
199
- break;
200
214
  }
201
215
  return this.toResult(handle);
202
216
  }
@@ -213,7 +227,7 @@ class StormEventParser {
213
227
  return this.error;
214
228
  }
215
229
  toResult(handle) {
216
- const planRef = StormEventParser.toRef(handle, this.planName ?? 'undefined');
230
+ const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
217
231
  const blockDefinitions = this.toBlockDefinitions(handle);
218
232
  const refIdMap = {};
219
233
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
@@ -225,7 +239,7 @@ class StormEventParser {
225
239
  block: {
226
240
  ref,
227
241
  },
228
- name: block.content.metadata.title ?? block.content.metadata.name,
242
+ name: block.content.metadata.title || block.content.metadata.name,
229
243
  dimensions: {
230
244
  left: 0,
231
245
  top: 0,
@@ -150,19 +150,35 @@ export interface StormEventScreenCandidate {
150
150
  url: string;
151
151
  };
152
152
  }
153
- export interface StormEventFile {
154
- type: 'FILE';
153
+ export interface StormEventFileBasePayload {
154
+ filename: string;
155
+ path: string;
156
+ blockName: string;
157
+ blockRef: string;
158
+ instanceId: string;
159
+ }
160
+ export interface StormEventFileBase {
161
+ type: string;
155
162
  reason: string;
156
163
  created: number;
157
- payload: {
158
- filename: string;
159
- path: string;
164
+ payload: StormEventFileBasePayload;
165
+ }
166
+ export interface StormEventFileLogical extends StormEventFileBase {
167
+ type: 'FILE_START' | 'FILE_CHUNK_RESET';
168
+ }
169
+ export interface StormEventFileState extends StormEventFileBase {
170
+ type: 'FILE_STATE';
171
+ payload: StormEventFileBasePayload & {
172
+ state: string;
173
+ };
174
+ }
175
+ export interface StormEventFileContent extends StormEventFileBase {
176
+ type: 'FILE_DONE' | 'FILE_CHUNK';
177
+ payload: StormEventFileBasePayload & {
160
178
  content: string;
161
- blockName: string;
162
- blockRef: string;
163
- instanceId: string;
164
179
  };
165
180
  }
181
+ export type StormEventFile = StormEventFileLogical | StormEventFileState | StormEventFileContent;
166
182
  export interface StormEventBlockReady {
167
183
  type: 'BLOCK_READY';
168
184
  reason: string;
@@ -184,4 +200,16 @@ export interface StormEventDefinitionChange {
184
200
  created: number;
185
201
  payload: StormDefinitions;
186
202
  }
187
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady;
203
+ export declare enum StormEventPhaseType {
204
+ META = "META",
205
+ DEFINITIONS = "DEFINITIONS",
206
+ IMPLEMENTATION = "IMPLEMENTATION"
207
+ }
208
+ export interface StormEventPhases {
209
+ type: 'PHASE_START' | 'PHASE_END';
210
+ created: number;
211
+ payload: {
212
+ phaseType: StormEventPhaseType;
213
+ };
214
+ }
215
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileContent | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StormEventPhaseType = void 0;
4
+ var StormEventPhaseType;
5
+ (function (StormEventPhaseType) {
6
+ StormEventPhaseType["META"] = "META";
7
+ StormEventPhaseType["DEFINITIONS"] = "DEFINITIONS";
8
+ StormEventPhaseType["IMPLEMENTATION"] = "IMPLEMENTATION";
9
+ })(StormEventPhaseType || (exports.StormEventPhaseType = StormEventPhaseType = {}));