@kapeta/local-cluster-service 0.48.5 → 0.50.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/assetManager.d.ts +1 -1
  3. package/dist/cjs/src/assetManager.js +5 -3
  4. package/dist/cjs/src/filesystem/routes.js +10 -0
  5. package/dist/cjs/src/storm/codegen.d.ts +4 -1
  6. package/dist/cjs/src/storm/codegen.js +63 -11
  7. package/dist/cjs/src/storm/event-parser.d.ts +5 -2
  8. package/dist/cjs/src/storm/event-parser.js +24 -4
  9. package/dist/cjs/src/storm/events.d.ts +24 -1
  10. package/dist/cjs/src/storm/events.js +7 -0
  11. package/dist/cjs/src/storm/routes.js +88 -6
  12. package/dist/cjs/src/storm/stormClient.js +5 -1
  13. package/dist/cjs/src/storm/stream.d.ts +11 -0
  14. package/dist/cjs/src/storm/stream.js +11 -0
  15. package/dist/esm/src/assetManager.d.ts +1 -1
  16. package/dist/esm/src/assetManager.js +5 -3
  17. package/dist/esm/src/filesystem/routes.js +10 -0
  18. package/dist/esm/src/storm/codegen.d.ts +4 -1
  19. package/dist/esm/src/storm/codegen.js +63 -11
  20. package/dist/esm/src/storm/event-parser.d.ts +5 -2
  21. package/dist/esm/src/storm/event-parser.js +24 -4
  22. package/dist/esm/src/storm/events.d.ts +24 -1
  23. package/dist/esm/src/storm/events.js +7 -0
  24. package/dist/esm/src/storm/routes.js +88 -6
  25. package/dist/esm/src/storm/stormClient.js +5 -1
  26. package/dist/esm/src/storm/stream.d.ts +11 -0
  27. package/dist/esm/src/storm/stream.js +11 -0
  28. package/package.json +1 -1
  29. package/src/assetManager.ts +6 -4
  30. package/src/filesystem/routes.ts +10 -0
  31. package/src/storm/codegen.ts +95 -22
  32. package/src/storm/event-parser.ts +33 -5
  33. package/src/storm/events.ts +32 -4
  34. package/src/storm/routes.ts +113 -12
  35. package/src/storm/stormClient.ts +8 -1
  36. package/src/storm/stream.ts +22 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.50.0](https://github.com/kapetacom/local-cluster-service/compare/v0.49.0...v0.50.0) (2024-06-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Handle aborted requests ([#162](https://github.com/kapetacom/local-cluster-service/issues/162)) ([a9323d4](https://github.com/kapetacom/local-cluster-service/commit/a9323d46423361c2de63e40b4b61927b9b4198b7))
7
+
8
+ # [0.49.0](https://github.com/kapetacom/local-cluster-service/compare/v0.48.5...v0.49.0) (2024-06-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * Implement ability to save ai generated plan ([#161](https://github.com/kapetacom/local-cluster-service/issues/161)) ([8223757](https://github.com/kapetacom/local-cluster-service/commit/8223757ff90ee6b5e1b7f171fc724d9877051df2))
14
+
1
15
  ## [0.48.5](https://github.com/kapetacom/local-cluster-service/compare/v0.48.4...v0.48.5) (2024-06-04)
2
16
 
3
17
 
@@ -28,7 +28,7 @@ declare class AssetManager {
28
28
  getPlan(ref: string, noCache?: boolean): Promise<Plan>;
29
29
  getBlockInstance(systemId: string, instanceId: string): Promise<BlockInstance>;
30
30
  getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
31
- createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange): Promise<EnrichedAsset[]>;
31
+ createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange, codegen?: boolean): Promise<EnrichedAsset[]>;
32
32
  updateAsset(ref: string, yaml: Definition, sourceOfChange?: SourceOfChange): Promise<void>;
33
33
  importFile(filePath: string): Promise<EnrichedAsset[]>;
34
34
  unregisterAsset(ref: string): Promise<void>;
@@ -142,7 +142,7 @@ class AssetManager {
142
142
  }
143
143
  return undefined;
144
144
  }
145
- async createAsset(path, yaml, sourceOfChange = 'filesystem') {
145
+ async createAsset(path, yaml, sourceOfChange = 'filesystem', codegen = true) {
146
146
  if (await fs_extra_1.default.pathExists(path)) {
147
147
  throw new Error('File already exists: ' + path);
148
148
  }
@@ -159,9 +159,11 @@ class AssetManager {
159
159
  cacheManager_1.cacheManager.set(key, a, CACHE_TTL);
160
160
  });
161
161
  definitionsManager_1.definitionsManager.clearCache();
162
- console.log(`Created asset at: ${path}`);
163
162
  const ref = `kapeta://${yaml.metadata.name}:local`;
164
- await this.maybeGenerateCode(ref, path, yaml);
163
+ console.log(`Created asset ${ref} at: ${path}`);
164
+ if (codegen) {
165
+ await this.maybeGenerateCode(ref, path, yaml);
166
+ }
165
167
  return asset;
166
168
  }
167
169
  async updateAsset(ref, yaml, sourceOfChange = 'filesystem') {
@@ -11,6 +11,7 @@ const express_promise_router_1 = __importDefault(require("express-promise-router
11
11
  const stringBody_1 = require("../middleware/stringBody");
12
12
  const filesystemManager_1 = require("../filesystemManager");
13
13
  const cors_1 = require("../middleware/cors");
14
+ const fs_extra_1 = __importDefault(require("fs-extra"));
14
15
  let router = (0, express_promise_router_1.default)();
15
16
  router.use('/', cors_1.corsHandler);
16
17
  router.get('/root', (req, res) => {
@@ -81,6 +82,15 @@ router.get('/readfile', async (req, res) => {
81
82
  res.status(400).send({ error: '' + err });
82
83
  }
83
84
  });
85
+ router.get('/exists', async (req, res) => {
86
+ let pathArg = req.query.path;
87
+ try {
88
+ res.send(await fs_extra_1.default.pathExists(pathArg));
89
+ }
90
+ catch (err) {
91
+ res.status(400).send({ error: '' + err });
92
+ }
93
+ });
84
94
  router.put('/mkdir', async (req, res) => {
85
95
  let pathArg = req.query.path;
86
96
  try {
@@ -11,8 +11,10 @@ export declare class StormCodegen {
11
11
  private readonly out;
12
12
  private readonly events;
13
13
  private readonly tmpDir;
14
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
14
+ private readonly conversationId;
15
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
15
16
  process(): Promise<void>;
17
+ isAborted(): boolean;
16
18
  getStream(): StormStream;
17
19
  private handleTemplateFileOutput;
18
20
  private handleUiOutput;
@@ -46,4 +48,5 @@ export declare class StormCodegen {
46
48
  * Generates the code using codegen for a given block.
47
49
  */
48
50
  private generateBlock;
51
+ abort(): void;
49
52
  }
@@ -36,28 +36,36 @@ const codeGeneratorManager_1 = require("../codeGeneratorManager");
36
36
  const stormClient_1 = require("./stormClient");
37
37
  const event_parser_1 = require("./event-parser");
38
38
  const stream_1 = require("./stream");
39
+ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
39
40
  const promises_1 = require("fs/promises");
40
41
  const path_1 = __importStar(require("path"));
41
42
  const node_os_1 = __importDefault(require("node:os"));
42
43
  const fs_1 = require("fs");
44
+ const path_2 = __importDefault(require("path"));
43
45
  class StormCodegen {
44
46
  userPrompt;
45
47
  blocks;
46
48
  out = new stream_1.StormStream();
47
49
  events;
48
50
  tmpDir;
49
- constructor(userPrompt, blocks, events) {
51
+ conversationId;
52
+ constructor(conversationId, userPrompt, blocks, events) {
50
53
  this.userPrompt = userPrompt;
51
54
  this.blocks = blocks;
52
55
  this.events = events;
53
- this.tmpDir = node_os_1.default.tmpdir();
56
+ this.tmpDir = path_2.default.join(node_os_1.default.tmpdir(), conversationId);
57
+ this.conversationId = conversationId;
54
58
  }
55
59
  async process() {
56
- for (const block of this.blocks) {
57
- await this.processBlockCode(block);
58
- }
60
+ const promises = this.blocks.map((block) => {
61
+ return this.processBlockCode(block);
62
+ });
63
+ await Promise.all(promises);
59
64
  this.out.end();
60
65
  }
66
+ isAborted() {
67
+ return this.out.isAborted();
68
+ }
61
69
  getStream() {
62
70
  return this.out;
63
71
  }
@@ -112,6 +120,9 @@ class StormCodegen {
112
120
  * Generates the code for a block and sends it to the AI
113
121
  */
114
122
  async processBlockCode(block) {
123
+ if (this.isAborted()) {
124
+ return;
125
+ }
115
126
  // Generate the code for the block using the standard codegen templates
116
127
  const generatedResult = await this.generateBlock(block.content);
117
128
  if (!generatedResult) {
@@ -119,7 +130,10 @@ class StormCodegen {
119
130
  }
120
131
  const allFiles = this.toStormFiles(generatedResult);
121
132
  // Send all the non-ai files to the stream
122
- this.emitFiles(block.uri, block.aiName, allFiles);
133
+ this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
134
+ if (this.isAborted()) {
135
+ return;
136
+ }
123
137
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
124
138
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
125
139
  if (uiTemplates.length > 0) {
@@ -131,18 +145,27 @@ class StormCodegen {
131
145
  prompt: this.userPrompt,
132
146
  });
133
147
  uiStream.on('data', (evt) => {
134
- this.handleUiOutput(block.uri, block.aiName, evt);
148
+ this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
149
+ });
150
+ this.out.on('aborted', () => {
151
+ uiStream.abort();
135
152
  });
136
153
  await uiStream.waitForDone();
137
154
  }
155
+ if (this.isAborted()) {
156
+ return;
157
+ }
138
158
  // Gather the context files for implementation. These will be all be passed to the AI
139
159
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
140
160
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
141
161
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
142
162
  if (serviceFiles.length > 0) {
143
- await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
163
+ await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
144
164
  }
145
165
  const basePath = this.getBasePath(block.content.metadata.name);
166
+ if (this.isAborted()) {
167
+ return;
168
+ }
146
169
  for (const serviceFile of serviceFiles) {
147
170
  const filePath = (0, path_1.join)(basePath, serviceFile.filename);
148
171
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -158,6 +181,18 @@ class StormCodegen {
158
181
  const filesToBeFixed = serviceFiles.concat(contextFiles);
159
182
  const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
160
183
  await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
184
+ const blockRef = block.uri;
185
+ this.out.emit('data', {
186
+ type: 'BLOCK_READY',
187
+ reason: 'Block ready',
188
+ created: Date.now(),
189
+ payload: {
190
+ path: basePath,
191
+ blockName: block.aiName,
192
+ blockRef,
193
+ instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef),
194
+ },
195
+ });
161
196
  }
162
197
  async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
163
198
  let attempts = 0;
@@ -175,6 +210,9 @@ class StormCodegen {
175
210
  console.debug('Validation error:', result);
176
211
  const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
177
212
  const fixes = new Map();
213
+ this.out.on('aborted', () => {
214
+ errorStream.abort();
215
+ });
178
216
  errorStream.on('data', (evt) => {
179
217
  if (evt.type === 'ERROR_CLASSIFIER') {
180
218
  // find the file that caused the error
@@ -186,9 +224,11 @@ class StormCodegen {
186
224
  }
187
225
  // read the content of the file
188
226
  const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
189
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles.map(e => e.filename).join("\n")}\n---\n${content}`;
190
- console.log(`trying to fix the code in ${eventFileName}`);
191
- console.debug(`with the fix:\n${fix}`);
227
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
228
+ .map((e) => e.filename)
229
+ .join('\n')}\n---\n${content}`;
230
+ //console.log(`trying to fix the code in ${eventFileName}`);
231
+ //console.debug(`with the fix:\n${fix}`);
192
232
  const code = this.codeFix(fix);
193
233
  fixes.set((0, path_1.join)(basePath, eventFileName), code);
194
234
  }
@@ -233,6 +273,9 @@ class StormCodegen {
233
273
  resolve(evt.payload.content);
234
274
  }
235
275
  });
276
+ this.out.on('aborted', () => {
277
+ fixStream.abort();
278
+ });
236
279
  fixStream.on('error', (err) => {
237
280
  reject(err);
238
281
  });
@@ -288,6 +331,9 @@ class StormCodegen {
288
331
  prompt: this.userPrompt,
289
332
  });
290
333
  const files = [];
334
+ this.out.on('aborted', () => {
335
+ stream.abort();
336
+ });
291
337
  stream.on('data', (evt) => {
292
338
  const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
293
339
  if (file) {
@@ -338,6 +384,9 @@ class StormCodegen {
338
384
  * Generates the code using codegen for a given block.
339
385
  */
340
386
  async generateBlock(yamlContent) {
387
+ if (this.isAborted()) {
388
+ return;
389
+ }
341
390
  if (!yamlContent.spec.target?.kind) {
342
391
  //Not all block types have targets
343
392
  return;
@@ -352,5 +401,8 @@ class StormCodegen {
352
401
  new codegen_1.CodeWriter(basePath).write(generatedResult);
353
402
  return generatedResult;
354
403
  }
404
+ abort() {
405
+ this.out.abort();
406
+ }
355
407
  }
356
408
  exports.StormCodegen = StormCodegen;
@@ -2,11 +2,11 @@
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 {
9
- uri: KapetaURI;
9
+ uri: string;
10
10
  content: BlockDefinition;
11
11
  aiName: string;
12
12
  }
@@ -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');
@@ -213,7 +231,7 @@ class StormEventParser {
213
231
  return this.error;
214
232
  }
215
233
  toResult(handle) {
216
- const planRef = StormEventParser.toRef(handle, this.planName ?? 'undefined');
234
+ const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
217
235
  const blockDefinitions = this.toBlockDefinitions(handle);
218
236
  const refIdMap = {};
219
237
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
@@ -225,7 +243,7 @@ class StormEventParser {
225
243
  block: {
226
244
  ref,
227
245
  },
228
- name: block.content.metadata.title ?? block.content.metadata.name,
246
+ name: block.content.metadata.title || block.content.metadata.name,
229
247
  dimensions: {
230
248
  left: 0,
231
249
  top: 0,
@@ -315,6 +333,8 @@ class StormEventParser {
315
333
  name: planRef.fullName,
316
334
  title: this.planName,
317
335
  description: this.planDescription,
336
+ structure: 'mono',
337
+ visibility: 'private',
318
338
  },
319
339
  spec: {
320
340
  blocks,
@@ -331,7 +351,7 @@ class StormEventParser {
331
351
  Object.entries(this.blocks).forEach(([, blockInfo]) => {
332
352
  const blockRef = StormEventParser.toRef(handle, blockInfo.name);
333
353
  const blockDefinitionInfo = {
334
- uri: blockRef,
354
+ uri: blockRef.toNormalizedString(),
335
355
  aiName: blockInfo.name,
336
356
  content: {
337
357
  kind: this.toBlockKind(blockInfo.type),
@@ -163,6 +163,17 @@ export interface StormEventFile {
163
163
  instanceId: string;
164
164
  };
165
165
  }
166
+ export interface StormEventBlockReady {
167
+ type: 'BLOCK_READY';
168
+ reason: string;
169
+ created: number;
170
+ payload: {
171
+ path: string;
172
+ blockName: string;
173
+ blockRef: string;
174
+ instanceId: string;
175
+ };
176
+ }
166
177
  export interface StormEventDone {
167
178
  type: 'DONE';
168
179
  created: number;
@@ -173,4 +184,16 @@ export interface StormEventDefinitionChange {
173
184
  created: number;
174
185
  payload: StormDefinitions;
175
186
  }
176
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix;
187
+ export declare enum StormEventPhaseType {
188
+ META = "META",
189
+ DEFINITIONS = "DEFINITIONS",
190
+ IMPLEMENTATION = "IMPLEMENTATION"
191
+ }
192
+ export interface StormEventPhases {
193
+ type: 'PHASE_START' | 'PHASE_END';
194
+ created: number;
195
+ payload: {
196
+ phaseType: StormEventPhaseType;
197
+ };
198
+ }
199
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | 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 = {}));
@@ -8,11 +8,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
12
  const cors_1 = require("../middleware/cors");
12
13
  const stringBody_1 = require("../middleware/stringBody");
13
14
  const stormClient_1 = require("./stormClient");
15
+ const events_1 = require("./events");
14
16
  const event_parser_1 = require("./event-parser");
15
17
  const codegen_1 = require("./codegen");
18
+ const assetManager_1 = require("../assetManager");
19
+ const path_1 = __importDefault(require("path"));
16
20
  const router = (0, express_promise_router_1.default)();
17
21
  router.use('/', cors_1.corsHandler);
18
22
  router.use('/', stringBody_1.stringBody);
@@ -24,15 +28,41 @@ router.post('/:handle/all', async (req, res) => {
24
28
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
25
29
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
26
30
  const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, conversationId);
31
+ onRequestAborted(req, res, () => {
32
+ metaStream.abort();
33
+ });
27
34
  res.set('Content-Type', 'application/x-ndjson');
28
35
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
29
36
  res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
37
+ let currentPhase = events_1.StormEventPhaseType.META;
30
38
  metaStream.on('data', (data) => {
31
39
  const result = eventParser.processEvent(req.params.handle, data);
40
+ switch (data.type) {
41
+ case 'CREATE_API':
42
+ case 'CREATE_MODEL':
43
+ case 'CREATE_TYPE':
44
+ if (currentPhase !== events_1.StormEventPhaseType.DEFINITIONS) {
45
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.META));
46
+ currentPhase = events_1.StormEventPhaseType.DEFINITIONS;
47
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.DEFINITIONS));
48
+ }
49
+ break;
50
+ }
32
51
  sendEvent(res, data);
33
52
  sendDefinitions(res, result);
34
53
  });
35
- await waitForStormStream(metaStream);
54
+ try {
55
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.META));
56
+ await waitForStormStream(metaStream);
57
+ }
58
+ finally {
59
+ if (!metaStream.isAborted()) {
60
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(currentPhase));
61
+ }
62
+ }
63
+ if (metaStream.isAborted()) {
64
+ return;
65
+ }
36
66
  if (!eventParser.isValid()) {
37
67
  // We can't continue if the meta stream is invalid
38
68
  sendEvent(res, {
@@ -45,18 +75,53 @@ router.post('/:handle/all', async (req, res) => {
45
75
  return;
46
76
  }
47
77
  const result = eventParser.toResult(handle);
78
+ if (metaStream.isAborted()) {
79
+ return;
80
+ }
48
81
  sendDefinitions(res, result);
49
82
  if (!req.query.skipCodegen) {
50
- const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
51
- const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
52
- await stormCodegen.process();
53
- await codegenPromise;
83
+ try {
84
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
85
+ const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
86
+ onRequestAborted(req, res, () => {
87
+ stormCodegen.abort();
88
+ });
89
+ const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
90
+ await stormCodegen.process();
91
+ await codegenPromise;
92
+ }
93
+ finally {
94
+ if (!metaStream.isAborted()) {
95
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
96
+ }
97
+ }
54
98
  }
55
99
  sendDone(res);
56
100
  }
57
101
  catch (err) {
58
102
  sendError(err, res);
59
- res.end();
103
+ if (!res.closed) {
104
+ res.end();
105
+ }
106
+ }
107
+ });
108
+ router.post('/block/create', async (req, res) => {
109
+ const createRequest = JSON.parse(req.stringBody ?? '{}');
110
+ try {
111
+ const ymlPath = path_1.default.join(createRequest.newPath, 'kapeta.yml');
112
+ const [asset] = await assetManager_1.assetManager.createAsset(ymlPath, createRequest.definition);
113
+ if (await fs_extra_1.default.pathExists(createRequest.tmpPath)) {
114
+ await fs_extra_1.default.move(createRequest.tmpPath, createRequest.newPath, {
115
+ overwrite: true,
116
+ });
117
+ res.send(await assetManager_1.assetManager.updateAsset(asset.ref, createRequest.definition));
118
+ }
119
+ else {
120
+ res.send(asset);
121
+ }
122
+ }
123
+ catch (err) {
124
+ res.status(500).send({ error: err.message });
60
125
  }
61
126
  });
62
127
  function sendDefinitions(res, result) {
@@ -68,6 +133,9 @@ function sendDefinitions(res, result) {
68
133
  });
69
134
  }
70
135
  function sendDone(res) {
136
+ if (res.closed) {
137
+ return;
138
+ }
71
139
  sendEvent(res, {
72
140
  type: 'DONE',
73
141
  created: Date.now(),
@@ -75,6 +143,9 @@ function sendDone(res) {
75
143
  res.end();
76
144
  }
77
145
  function sendError(err, res) {
146
+ if (res.closed) {
147
+ return;
148
+ }
78
149
  console.error('Failed to send prompt', err);
79
150
  if (res.headersSent) {
80
151
  sendEvent(res, {
@@ -112,6 +183,17 @@ function streamStormPartialResponse(result, res) {
112
183
  });
113
184
  }
114
185
  function sendEvent(res, evt) {
186
+ if (res.closed) {
187
+ return;
188
+ }
115
189
  res.write(JSON.stringify(evt) + '\n');
116
190
  }
191
+ function onRequestAborted(req, res, onAborted) {
192
+ req.on('close', () => {
193
+ onAborted();
194
+ });
195
+ res.on('close', () => {
196
+ onAborted();
197
+ });
198
+ }
117
199
  exports.default = router;
@@ -46,12 +46,13 @@ class StormClient {
46
46
  prompt: stringPrompt,
47
47
  conversationId: body.conversationId,
48
48
  });
49
+ const abort = new AbortController();
50
+ options.signal = abort.signal;
49
51
  const response = await fetch(options.url, options);
50
52
  if (response.status !== 200) {
51
53
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
52
54
  }
53
55
  const conversationId = response.headers.get(exports.ConversationIdHeader);
54
- console.log('Received conversationId', conversationId);
55
56
  const out = new stream_1.StormStream(stringPrompt, conversationId);
56
57
  const jsonLStream = promises_1.default.createInterface(node_stream_1.Readable.fromWeb(response.body));
57
58
  jsonLStream.on('line', (line) => {
@@ -63,6 +64,9 @@ class StormClient {
63
64
  jsonLStream.on('close', () => {
64
65
  out.end();
65
66
  });
67
+ out.on('aborted', () => {
68
+ abort.abort();
69
+ });
66
70
  return out;
67
71
  }
68
72
  createMetadata(prompt, conversationId) {
@@ -6,20 +6,26 @@
6
6
  import { EventEmitter } from 'node:events';
7
7
  import { StormEvent } from './events';
8
8
  import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
9
+ import { BlockDefinition } from '@kapeta/schemas';
9
10
  export declare class StormStream extends EventEmitter {
10
11
  private conversationId;
11
12
  private lines;
13
+ private aborted;
12
14
  constructor(prompt?: string, conversationId?: string | null);
13
15
  getConversationId(): string;
16
+ isAborted(): boolean;
14
17
  addJSONLine(line: string): void;
15
18
  end(): void;
16
19
  on(event: 'end', listener: () => void): this;
20
+ on(event: 'aborted', listener: () => void): this;
17
21
  on(event: 'error', listener: (e: Error) => void): this;
18
22
  on(event: 'data', listener: (data: StormEvent) => void): this;
19
23
  emit(event: 'end'): boolean;
24
+ emit(event: 'aborted'): void;
20
25
  emit(event: 'error', e: Error): boolean;
21
26
  emit(event: 'data', data: StormEvent): boolean;
22
27
  waitForDone(): Promise<void>;
28
+ abort(): void;
23
29
  }
24
30
  export interface ConversationItem {
25
31
  role: 'user' | 'model';
@@ -29,6 +35,11 @@ export interface StormContextRequest<T = string> {
29
35
  conversationId?: string;
30
36
  prompt: T;
31
37
  }
38
+ export interface StormCreateBlockRequest {
39
+ definition: BlockDefinition;
40
+ tmpPath: string;
41
+ newPath: string;
42
+ }
32
43
  export interface StormFileInfo extends GeneratedFile {
33
44
  type: AIFileTypes;
34
45
  }
@@ -9,6 +9,7 @@ const node_events_1 = require("node:events");
9
9
  class StormStream extends node_events_1.EventEmitter {
10
10
  conversationId = '';
11
11
  lines = [];
12
+ aborted = false;
12
13
  constructor(prompt = '', conversationId) {
13
14
  super();
14
15
  this.conversationId = conversationId || '';
@@ -16,6 +17,9 @@ class StormStream extends node_events_1.EventEmitter {
16
17
  getConversationId() {
17
18
  return this.conversationId;
18
19
  }
20
+ isAborted() {
21
+ return this.aborted;
22
+ }
19
23
  addJSONLine(line) {
20
24
  try {
21
25
  this.lines.push(line);
@@ -48,5 +52,12 @@ class StormStream extends node_events_1.EventEmitter {
48
52
  });
49
53
  });
50
54
  }
55
+ abort() {
56
+ if (this.aborted) {
57
+ return;
58
+ }
59
+ this.aborted = true;
60
+ this.emit('aborted');
61
+ }
51
62
  }
52
63
  exports.StormStream = StormStream;
@@ -28,7 +28,7 @@ declare class AssetManager {
28
28
  getPlan(ref: string, noCache?: boolean): Promise<Plan>;
29
29
  getBlockInstance(systemId: string, instanceId: string): Promise<BlockInstance>;
30
30
  getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
31
- createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange): Promise<EnrichedAsset[]>;
31
+ createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange, codegen?: boolean): Promise<EnrichedAsset[]>;
32
32
  updateAsset(ref: string, yaml: Definition, sourceOfChange?: SourceOfChange): Promise<void>;
33
33
  importFile(filePath: string): Promise<EnrichedAsset[]>;
34
34
  unregisterAsset(ref: string): Promise<void>;