@kapeta/local-cluster-service 0.53.4 → 0.54.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/.eslintrc.cjs CHANGED
@@ -6,6 +6,7 @@ module.exports = {
6
6
  rules: {
7
7
  '@typescript-eslint/no-explicit-any': 'off',
8
8
  '@typescript-eslint/no-non-null-assertion': 'off',
9
+ '@typescript-eslint/no-use-before-define': 'off',
9
10
  '@typescript-eslint/no-unsafe-assignment': 'off',
10
11
  '@typescript-eslint/no-unsafe-member-access': 'off',
11
12
  '@typescript-eslint/no-unsafe-return': 'off',
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ # [0.54.0](https://github.com/kapetacom/local-cluster-service/compare/v0.53.5...v0.54.0) (2024-06-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * add support for split UI routes + screen context in AI service ([#178](https://github.com/kapetacom/local-cluster-service/issues/178)) ([4330a72](https://github.com/kapetacom/local-cluster-service/commit/4330a72eddb5fe82f9e7a9c2af412d47091cf9b0))
7
+
8
+ ## [0.53.5](https://github.com/kapetacom/local-cluster-service/compare/v0.53.4...v0.53.5) (2024-06-17)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * actually map to correct api ([dc88af0](https://github.com/kapetacom/local-cluster-service/commit/dc88af0f7a76867ed9f96e5653a1ceb7f9002f6d))
14
+ * pretty ([9efc6ed](https://github.com/kapetacom/local-cluster-service/commit/9efc6edd9bb7717889d37a2d1c0c227b10e57d47))
15
+ * split apis into multiple named resources ([da3b59f](https://github.com/kapetacom/local-cluster-service/commit/da3b59fb238f2035235b452491f3b170768e49d9))
16
+ * use type to determine controller or not ([bb88e9f](https://github.com/kapetacom/local-cluster-service/commit/bb88e9f0d349bc23fc7973e8525578e6dcb1d663))
17
+
1
18
  ## [0.53.4](https://github.com/kapetacom/local-cluster-service/compare/v0.53.3...v0.53.4) (2024-06-13)
2
19
 
3
20
 
@@ -44,7 +44,6 @@ const path_2 = __importStar(require("path"));
44
44
  const node_os_1 = __importDefault(require("node:os"));
45
45
  const fs_1 = require("fs");
46
46
  const yaml_1 = __importDefault(require("yaml"));
47
- const fs = __importStar(require("node:fs"));
48
47
  const SIMULATED_DELAY = 1000;
49
48
  const ENABLE_SIMULATED_DELAY = false;
50
49
  class SimulatedFileDelay {
@@ -239,8 +238,11 @@ class StormCodegen {
239
238
  return;
240
239
  }
241
240
  const blockUri = (0, nodejs_utils_1.parseKapetaUri)(block.uri);
242
- const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
241
+ const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE &&
242
+ file.type !== codegen_1.AIFileTypes.WEB_SCREEN &&
243
+ file.type !== codegen_1.AIFileTypes.WEB_ROUTER);
243
244
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
245
+ const webRouters = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_ROUTER);
244
246
  const screenFiles = [];
245
247
  let filteredEvents = [];
246
248
  for (const event of this.events) {
@@ -249,14 +251,55 @@ class StormCodegen {
249
251
  filteredEvents = [];
250
252
  }
251
253
  }
252
- if (uiTemplates.length > 0) {
253
- const uiStream = await stormClient_1.stormClient.createUIImplementation({
254
+ const screenEvents = [];
255
+ // generate screens
256
+ const screenStream = await stormClient_1.stormClient.listScreens({
257
+ events: filteredEvents,
258
+ templates: uiTemplates,
259
+ context: relevantFiles,
260
+ blockName: block.aiName,
261
+ prompt: this.userPrompt,
262
+ });
263
+ screenStream.on('data', (evt) => {
264
+ if (evt.type === 'SCREEN') {
265
+ screenEvents.push(evt);
266
+ }
267
+ this.handleUiOutput(blockUri, block.aiName, evt);
268
+ });
269
+ this.out.on('aborted', () => {
270
+ screenStream.abort();
271
+ });
272
+ await screenStream.waitForDone();
273
+ // screenfiles
274
+ const screenTemplates = screenEvents
275
+ .map((screenEvent) => ({
276
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
277
+ filename: screenEvent.payload.filename,
278
+ }))
279
+ .filter((tpl) => !!tpl.content);
280
+ await Promise.all(screenTemplates.concat(webRouters).map(async (template) => {
281
+ const payload = {
254
282
  events: filteredEvents,
255
- templates: uiTemplates,
256
- context: relevantFiles,
257
283
  blockName: block.aiName,
284
+ filename: template.filename,
285
+ template: template,
286
+ context: relevantFiles.concat([
287
+ {
288
+ type: codegen_1.AIFileTypes.INSTRUCTIONS,
289
+ mode: codegen_1.MODE_CREATE_ONLY,
290
+ permissions: '0644',
291
+ filename: '<screens>.md',
292
+ content: `
293
+ # Generated screens
294
+
295
+ ${JSON.stringify({ screenEvents })}
296
+
297
+ `,
298
+ },
299
+ ]),
258
300
  prompt: this.userPrompt,
259
- });
301
+ };
302
+ const uiStream = await stormClient_1.stormClient.createUIImplementation(payload);
260
303
  uiStream.on('data', (evt) => {
261
304
  const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
262
305
  if (uiFile != undefined) {
@@ -267,21 +310,34 @@ class StormCodegen {
267
310
  uiStream.abort();
268
311
  });
269
312
  await uiStream.waitForDone();
270
- }
313
+ }));
271
314
  if (this.isAborted()) {
272
315
  return;
273
316
  }
317
+ const basePath = this.getBasePath(block.content.metadata.name);
318
+ const screenFilesConverted = screenFiles.map((screenFile) => {
319
+ return {
320
+ filename: screenFile.payload.filename,
321
+ content: screenFile.payload.content,
322
+ mode: codegen_1.MODE_CREATE_ONLY,
323
+ permissions: '0644',
324
+ type: codegen_1.AIFileTypes.WEB_SCREEN,
325
+ };
326
+ });
274
327
  // Gather the context files for implementation. These will be all be passed to the AI
275
- const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
328
+ const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
276
329
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
277
330
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
278
331
  if (serviceFiles.length > 0) {
279
332
  await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
280
333
  }
281
- const basePath = this.getBasePath(block.content.metadata.name);
282
334
  if (this.isAborted()) {
283
335
  return;
284
336
  }
337
+ for (const screenFile of screenFilesConverted) {
338
+ const filePath = (0, path_2.join)(basePath, screenFile.filename);
339
+ await (0, promises_1.writeFile)(filePath, screenFile.content);
340
+ }
285
341
  for (const serviceFile of serviceFiles) {
286
342
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
287
343
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -296,16 +352,6 @@ class StormCodegen {
296
352
  const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
297
353
  await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
298
354
  }
299
- const screenFilesConverted = screenFiles.map((screenFile) => {
300
- return {
301
- filename: screenFile.payload.filename,
302
- content: screenFile.payload.content,
303
- mode: codegen_1.MODE_CREATE_ONLY,
304
- permissions: '0644',
305
- type: codegen_1.AIFileTypes.WEB_SCREEN,
306
- };
307
- });
308
- allFiles.push(...screenFilesConverted);
309
355
  const blockRef = block.uri;
310
356
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
311
357
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
@@ -466,7 +512,7 @@ class StormCodegen {
466
512
  const files = new Set(filesForContext);
467
513
  files.add(filename);
468
514
  const requestedFiles = Array.from(files).flatMap((file) => {
469
- if (fs.existsSync(file)) {
515
+ if ((0, fs_1.existsSync)(file)) {
470
516
  return file;
471
517
  }
472
518
  // file does not exist - look for similar
@@ -551,7 +597,7 @@ class StormCodegen {
551
597
  // They will need to be implemented by the AI
552
598
  return;
553
599
  }
554
- if (file.type === codegen_1.AIFileTypes.WEB_SCREEN) {
600
+ if ([codegen_1.AIFileTypes.WEB_ROUTER, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type)) {
555
601
  // Don't send the web screen files to the stream yet
556
602
  // They will need to be implemented by the AI
557
603
  return;
@@ -211,13 +211,13 @@ class StormEventParser {
211
211
  this.connections.push(evt.payload);
212
212
  break;
213
213
  case 'API_RETRY':
214
- Object.values(this.blocks).forEach(block => {
214
+ Object.values(this.blocks).forEach((block) => {
215
215
  block.types = [];
216
216
  block.apis = [];
217
217
  });
218
218
  break;
219
219
  case 'MODEL_RETRY':
220
- Object.values(this.blocks).forEach(block => {
220
+ Object.values(this.blocks).forEach((block) => {
221
221
  block.models = [];
222
222
  });
223
223
  break;
@@ -384,7 +384,7 @@ class StormEventParser {
384
384
  },
385
385
  };
386
386
  const blockSpec = blockDefinitionInfo.content.spec;
387
- let apiResource = undefined;
387
+ const apiResources = {};
388
388
  let dbResource = undefined;
389
389
  blockInfo.resources.forEach((resource) => {
390
390
  const port = {
@@ -392,10 +392,7 @@ class StormEventParser {
392
392
  };
393
393
  switch (resource.type) {
394
394
  case 'API':
395
- if (apiResource) {
396
- break;
397
- }
398
- apiResource = {
395
+ const apiResource = {
399
396
  kind: this.toResourceKind(resource.type),
400
397
  metadata: {
401
398
  name: resource.name,
@@ -411,6 +408,7 @@ class StormEventParser {
411
408
  },
412
409
  },
413
410
  };
411
+ apiResources[resource.name] = apiResource;
414
412
  blockSpec.providers.push(apiResource);
415
413
  break;
416
414
  case 'CLIENT':
@@ -491,11 +489,28 @@ class StormEventParser {
491
489
  });
492
490
  }
493
491
  });
494
- if (apiResource) {
495
- blockInfo.apis.forEach((api) => {
496
- apiResource.spec.source.value += api + '\n\n';
492
+ blockInfo.apis.forEach((api) => {
493
+ const dslApi = kaplang_core_1.DSLAPIParser.parse(api, {
494
+ ignoreSemantics: true,
497
495
  });
498
- }
496
+ let exactMatch = false;
497
+ if (dslApi[0] && dslApi[0].type == kaplang_core_1.DSLEntityType.CONTROLLER) {
498
+ const name = dslApi[0].name.toLowerCase();
499
+ const apiResourceName = Object.keys(apiResources).find((key) => key.indexOf(name) > -1);
500
+ if (apiResourceName) {
501
+ const exactResource = apiResources[apiResourceName];
502
+ exactResource.spec.source.value += api + '\n\n';
503
+ exactMatch = true;
504
+ }
505
+ }
506
+ if (!exactMatch) {
507
+ // if we couldn't place the given api on the exact resource we just park it on the first
508
+ // available rest resource
509
+ const firstKey = Object.keys(apiResources)[0];
510
+ const firstEntry = apiResources[firstKey];
511
+ firstEntry.spec.source.value += api + '\n\n';
512
+ }
513
+ });
499
514
  blockInfo.types.forEach((type) => {
500
515
  blockSpec.entities.source.value += type + '\n';
501
516
  });
@@ -159,6 +159,7 @@ export interface StormEventScreen {
159
159
  template: string;
160
160
  description: string;
161
161
  url: string;
162
+ filename: string;
162
163
  };
163
164
  }
164
165
  export interface StormEventScreenCandidate {
@@ -1,4 +1,4 @@
1
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -7,6 +7,7 @@ declare class StormClient {
7
7
  private createOptions;
8
8
  private send;
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
+ listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
10
11
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
12
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
13
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
@@ -80,6 +80,12 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ listScreens(prompt, conversationId) {
84
+ return this.send('/v2/ui/list', {
85
+ prompt,
86
+ conversationId,
87
+ });
88
+ }
83
89
  createUIImplementation(prompt, conversationId) {
84
90
  return this.send('/v2/ui/merge', {
85
91
  prompt,
@@ -50,6 +50,14 @@ export interface StormFileImplementationPrompt {
50
50
  prompt: string;
51
51
  }
52
52
  export interface StormUIImplementationPrompt {
53
+ events: StormEvent[];
54
+ template: StormFileInfo;
55
+ filename: string;
56
+ context: StormFileInfo[];
57
+ blockName: string;
58
+ prompt: string;
59
+ }
60
+ export interface StormUIListPrompt {
53
61
  events: StormEvent[];
54
62
  templates: StormFileInfo[];
55
63
  context: StormFileInfo[];
@@ -3,8 +3,12 @@
3
3
  * Copyright 2023 Kapeta Inc.
4
4
  * SPDX-License-Identifier: BUSL-1.1
5
5
  */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
6
9
  Object.defineProperty(exports, "__esModule", { value: true });
7
10
  const event_parser_1 = require("../../src/storm/event-parser");
11
+ const simple_blog_events_json_1 = __importDefault(require("./simple-blog-events.json"));
8
12
  const parserOptions = {
9
13
  serviceKind: 'kapeta/block-service:local',
10
14
  serviceLanguage: 'kapeta/language-target-nodejs-ts:local',
@@ -158,4 +162,18 @@ describe('event-parser', () => {
158
162
  expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
159
163
  expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
160
164
  });
165
+ it('it will split api into correct provider', () => {
166
+ const events = simple_blog_events_json_1.default;
167
+ const parser = new event_parser_1.StormEventParser(parserOptions);
168
+ events.forEach((event) => parser.processEvent('kapeta', event));
169
+ const result = parser.toResult('kapeta');
170
+ const blogService = result.blocks.find((block) => block.aiName === 'blog-service');
171
+ expect(blogService).toBeDefined();
172
+ expect(blogService?.content).toBeDefined();
173
+ const apiProviders = blogService?.content?.spec?.providers?.filter((provider) => provider.kind === 'kapeta/block-type-api:local');
174
+ expect(apiProviders).toBeDefined();
175
+ expect(apiProviders.length).toBe(2);
176
+ expect(apiProviders["0"].spec.source.value).not.toBe('');
177
+ expect(apiProviders["1"].spec.source.value).not.toBe('');
178
+ });
161
179
  });