@kapeta/local-cluster-service 0.46.0 → 0.47.1

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.
@@ -8,7 +8,7 @@ import { AIFileTypes, BlockCodeGenerator, GeneratedFile, GeneratedResult } from
8
8
  import { BlockDefinition } from '@kapeta/schemas';
9
9
  import { codeGeneratorManager } from '../codeGeneratorManager';
10
10
  import { STORM_ID, stormClient } from './stormClient';
11
- import { ScreenTemplate, StormEvent, StormEventFile, StormEventScreen } from './events';
11
+ import { StormEvent, StormEventFile } from './events';
12
12
  import { BlockDefinitionInfo } from './event-parser';
13
13
  import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
14
14
  import { KapetaURI } from '@kapeta/nodejs-utils';
@@ -42,16 +42,16 @@ export class StormCodegen {
42
42
  return this.out;
43
43
  }
44
44
 
45
- private handleTemplateFileOutput(blockUri: KapetaURI, template: StormFileInfo, data: StormEvent) {
45
+ private handleTemplateFileOutput(blockUri: KapetaURI, aiName: string, template: StormFileInfo, data: StormEvent) {
46
46
  switch (data.type) {
47
47
  case 'FILE':
48
48
  template.filename = data.payload.filename;
49
49
  template.content = data.payload.content;
50
- return this.handleFileOutput(blockUri, data);
50
+ return this.handleFileOutput(blockUri, aiName, data);
51
51
  }
52
52
  }
53
53
 
54
- private handleUiOutput(blockUri: KapetaURI, data: StormEvent) {
54
+ private handleUiOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
55
55
  switch (data.type) {
56
56
  case 'SCREEN':
57
57
  this.out.emit('data', {
@@ -60,18 +60,18 @@ export class StormCodegen {
60
60
  created: Date.now(),
61
61
  payload: {
62
62
  ...data.payload,
63
- blockName: blockUri.toNormalizedString(),
63
+ blockName: aiName,
64
64
  },
65
65
  });
66
66
  case 'FILE':
67
- return this.handleFileOutput(blockUri, data);
67
+ return this.handleFileOutput(blockUri, aiName, data);
68
68
  }
69
69
  }
70
70
 
71
- private handleFileOutput(blockUri: KapetaURI, data: StormEvent) {
71
+ private handleFileOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
72
72
  switch (data.type) {
73
73
  case 'FILE':
74
- this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
74
+ this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
75
75
  return {
76
76
  type: 'FILE',
77
77
  created: Date.now(),
@@ -96,7 +96,7 @@ export class StormCodegen {
96
96
  const allFiles = this.toStormFiles(generatedResult);
97
97
 
98
98
  // Send all the non-ai files to the stream
99
- this.emitFiles(block.uri, allFiles);
99
+ this.emitFiles(block.uri, block.aiName, allFiles);
100
100
 
101
101
  const relevantFiles: StormFileInfo[] = allFiles.filter(
102
102
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -112,7 +112,7 @@ export class StormCodegen {
112
112
  });
113
113
 
114
114
  uiStream.on('data', (evt) => {
115
- this.handleUiOutput(block.uri, evt);
115
+ this.handleUiOutput(block.uri, block.aiName, evt);
116
116
  });
117
117
 
118
118
  await uiStream.waitForDone();
@@ -128,6 +128,7 @@ export class StormCodegen {
128
128
  if (serviceFiles.length > 0) {
129
129
  await this.processTemplates(
130
130
  block.uri,
131
+ block.aiName,
131
132
  stormClient.createServiceImplementation.bind(stormClient),
132
133
  serviceFiles,
133
134
  contextFiles
@@ -138,7 +139,7 @@ export class StormCodegen {
138
139
  /**
139
140
  * Emits the text-based files to the stream
140
141
  */
141
- private emitFiles(uri: KapetaURI, files: StormFileInfo[]) {
142
+ private emitFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
142
143
  files.forEach((file) => {
143
144
  if (!file.content || typeof file.content !== 'string') {
144
145
  return;
@@ -156,11 +157,17 @@ export class StormCodegen {
156
157
  return;
157
158
  }
158
159
 
159
- this.emitFile(uri, file.filename, file.content);
160
+ this.emitFile(uri, aiName, file.filename, file.content);
160
161
  });
161
162
  }
162
163
 
163
- private emitFile(uri: KapetaURI, filename: string, content: string, reason: string = 'File generated') {
164
+ private emitFile(
165
+ uri: KapetaURI,
166
+ blockName: string,
167
+ filename: string,
168
+ content: string,
169
+ reason: string = 'File generated'
170
+ ) {
164
171
  this.out.emit('data', {
165
172
  type: 'FILE',
166
173
  reason,
@@ -168,6 +175,7 @@ export class StormCodegen {
168
175
  payload: {
169
176
  filename: filename,
170
177
  content: content,
178
+ blockName,
171
179
  blockRef: uri.toNormalizedString(),
172
180
  },
173
181
  } satisfies StormEventFile);
@@ -178,6 +186,7 @@ export class StormCodegen {
178
186
  */
179
187
  private async processTemplates(
180
188
  blockUri: KapetaURI,
189
+ aiName: string,
181
190
  generator: ImplementationGenerator,
182
191
  templates: StormFileInfo[],
183
192
  contextFiles: StormFileInfo[]
@@ -192,7 +201,7 @@ export class StormCodegen {
192
201
  const files: StormEventFile[] = [];
193
202
 
194
203
  stream.on('data', (evt) => {
195
- const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
204
+ const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
196
205
  if (file) {
197
206
  files.push(file);
198
207
  }
@@ -3,14 +3,7 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import {
7
- ScreenTemplate,
8
- StormBlockInfoFilled,
9
- StormBlockType,
10
- StormConnection,
11
- StormEvent,
12
- StormResourceType,
13
- } from './events';
6
+ import { StormBlockInfoFilled, StormBlockType, StormConnection, StormEvent, StormResourceType } from './events';
14
7
  import {
15
8
  BlockDefinition,
16
9
  BlockInstance,
@@ -21,8 +14,17 @@ import {
21
14
  SourceCode,
22
15
  } from '@kapeta/schemas';
23
16
  import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
24
- import { KAPLANG_ID, KAPLANG_VERSION, RESTMethod } from '@kapeta/kaplang-core';
25
- import uuid from 'node-uuid';
17
+ import {
18
+ DSLAPIParser,
19
+ DSLController,
20
+ DSLConverters,
21
+ DSLDataTypeParser,
22
+ DSLMethod,
23
+ KAPLANG_ID,
24
+ KAPLANG_VERSION,
25
+ KaplangWriter,
26
+ } from '@kapeta/kaplang-core';
27
+ import { v5 as uuid } from 'uuid';
26
28
  import { definitionsManager } from '../definitionsManager';
27
29
 
28
30
  export interface BlockDefinitionInfo {
@@ -31,7 +33,7 @@ export interface BlockDefinitionInfo {
31
33
  aiName: string;
32
34
  }
33
35
 
34
- export interface ParsedResult {
36
+ export interface StormDefinitions {
35
37
  plan: Plan;
36
38
  blocks: BlockDefinitionInfo[];
37
39
  }
@@ -196,8 +198,9 @@ export class StormEventParser {
196
198
  this.connections = [];
197
199
  }
198
200
 
199
- public addEvent(evt: StormEvent): void {
201
+ public addEvent(handle: string, evt: StormEvent): StormDefinitions {
200
202
  this.events.push(evt);
203
+ console.log('evt', evt);
201
204
  switch (evt.type) {
202
205
  case 'CREATE_PLAN_PROPERTIES':
203
206
  this.planName = evt.payload.name;
@@ -236,6 +239,8 @@ export class StormEventParser {
236
239
  case 'FILE':
237
240
  break;
238
241
  }
242
+
243
+ return this.toResult(handle);
239
244
  }
240
245
 
241
246
  public getEvents(): StormEvent[] {
@@ -243,6 +248,9 @@ export class StormEventParser {
243
248
  }
244
249
 
245
250
  public isValid(): boolean {
251
+ if (!this.planName) {
252
+ return false;
253
+ }
246
254
  return !this.failed;
247
255
  }
248
256
 
@@ -250,17 +258,13 @@ export class StormEventParser {
250
258
  return this.error;
251
259
  }
252
260
 
253
- private applyLayoutToBlocks(result: ParsedResult): ParsedResult {
254
- return result;
255
- }
256
-
257
- public toResult(handle: string): ParsedResult {
258
- const planRef = this.toRef(handle, this.planName);
261
+ public toResult(handle: string): StormDefinitions {
262
+ const planRef = this.toRef(handle, this.planName ?? 'undefined');
259
263
  const blockDefinitions = this.toBlockDefinitions(handle);
260
264
  const refIdMap: { [key: string]: string } = {};
261
- const screens: { [key: string]: ScreenTemplate[] } = {};
262
265
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
263
- const id = uuid.v4();
266
+ // Create a deterministic uuid
267
+ const id = uuid(ref, uuid.URL);
264
268
  refIdMap[ref] = id;
265
269
  return {
266
270
  id,
@@ -271,7 +275,7 @@ export class StormEventParser {
271
275
  dimensions: {
272
276
  left: 0,
273
277
  top: 0,
274
- width: 200,
278
+ width: 150,
275
279
  height: 200,
276
280
  },
277
281
  } satisfies BlockInstance;
@@ -327,13 +331,41 @@ export class StormEventParser {
327
331
  return;
328
332
  }
329
333
 
334
+ if (apiProviderBlock.content.spec.entities?.source?.value) {
335
+ if (!clientConsumerBlock.content.spec.entities) {
336
+ clientConsumerBlock.content.spec.entities = {
337
+ types: [],
338
+ source: {
339
+ type: KAPLANG_ID,
340
+ version: KAPLANG_VERSION,
341
+ value: '',
342
+ },
343
+ };
344
+ }
345
+
346
+ const clientTypes = DSLDataTypeParser.parse(
347
+ clientConsumerBlock.content.spec.entities.source!.value
348
+ );
349
+ const apiTypes = DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
350
+
351
+ apiTypes.forEach((apiType) => {
352
+ if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
353
+ // Already exists
354
+ return;
355
+ }
356
+ clientTypes.push(apiType);
357
+ });
358
+
359
+ clientConsumerBlock.content.spec.entities.source!.value = KaplangWriter.write(clientTypes);
360
+ }
330
361
  clientResource.spec.methods = apiResource.spec.methods;
331
362
  clientResource.spec.source = apiResource.spec.source;
332
363
  });
333
364
 
334
- const connections = this.connections.map((connection) => {
365
+ const connections: Connection[] = this.connections.map((connection) => {
335
366
  const fromRef = this.toRef(handle, connection.fromComponent);
336
367
  const toRef = this.toRef(handle, connection.toComponent);
368
+
337
369
  return {
338
370
  port: {
339
371
  type: this.toPortType(connection.fromResourceType),
@@ -346,9 +378,7 @@ export class StormEventParser {
346
378
  blockId: refIdMap[fromRef.toNormalizedString()],
347
379
  resourceName: connection.fromResource,
348
380
  },
349
- mapping: {
350
- //TODO: Add mapping
351
- },
381
+ mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
352
382
  } satisfies Connection;
353
383
  });
354
384
 
@@ -365,10 +395,10 @@ export class StormEventParser {
365
395
  },
366
396
  };
367
397
 
368
- return this.applyLayoutToBlocks({
398
+ return {
369
399
  plan,
370
400
  blocks: Object.values(blockDefinitions),
371
- });
401
+ };
372
402
  }
373
403
 
374
404
  private toSafeName(name: string): string {
@@ -504,7 +534,7 @@ export class StormEventParser {
504
534
  } satisfies SourceCode,
505
535
  },
506
536
  };
507
- blockSpec.providers!.push(dbResource);
537
+ blockSpec.consumers!.push(dbResource);
508
538
  break;
509
539
  case 'JWTCONSUMER':
510
540
  case 'WEBFRAGMENT':
@@ -596,6 +626,55 @@ export class StormEventParser {
596
626
  return '';
597
627
  }
598
628
 
629
+ private toConnectionMapping(
630
+ handle: string,
631
+ connection: StormConnection,
632
+ blockDefinitions: { [key: string]: BlockDefinitionInfo }
633
+ ): any {
634
+ if (connection.fromResourceType !== 'API') {
635
+ return;
636
+ }
637
+
638
+ const fromRef = this.toRef(handle, connection.fromComponent);
639
+
640
+ const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
641
+ if (!apiProviderBlock) {
642
+ console.warn('Provider block not found: %s', connection.fromComponent, connection);
643
+ return;
644
+ }
645
+
646
+ const apiResource = apiProviderBlock.content.spec.providers?.find(
647
+ (p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource
648
+ );
649
+
650
+ if (!apiResource) {
651
+ console.warn(
652
+ 'API resource not found: %s on %s',
653
+ connection.fromResource,
654
+ fromRef.toNormalizedString(),
655
+ connection
656
+ );
657
+ return;
658
+ }
659
+
660
+ const apiMethods = DSLConverters.toSchemaMethods(
661
+ DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
662
+ ignoreSemantics: true,
663
+ }) as (DSLMethod | DSLController)[]
664
+ );
665
+
666
+ const mapping: any = {};
667
+
668
+ Object.entries(apiMethods).forEach(([methodId, method]) => {
669
+ mapping[methodId] = {
670
+ targetId: methodId,
671
+ type: 'EXACT',
672
+ };
673
+ });
674
+
675
+ return mapping;
676
+ }
677
+
599
678
  private toPortType(type: StormResourceType) {
600
679
  switch (type) {
601
680
  case 'API':
@@ -2,6 +2,7 @@
2
2
  * Copyright 2023 Kapeta Inc.
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
+ import { StormDefinitions } from './event-parser';
5
6
 
6
7
  export type StormResourceType =
7
8
  | 'API'
@@ -145,6 +146,7 @@ export interface StormEventFile {
145
146
  payload: {
146
147
  filename: string;
147
148
  content: string;
149
+ blockName: string;
148
150
  blockRef: string;
149
151
  };
150
152
  }
@@ -154,6 +156,13 @@ export interface StormEventDone {
154
156
  created: number;
155
157
  }
156
158
 
159
+ export interface StormEventDefinitionChange {
160
+ type: 'DEFINITION_CHANGE';
161
+ reason: string;
162
+ created: number;
163
+ payload: StormDefinitions;
164
+ }
165
+
157
166
  export type StormEvent =
158
167
  | StormEventCreateBlock
159
168
  | StormEventCreateConnection
@@ -165,4 +174,5 @@ export type StormEvent =
165
174
  | StormEventScreen
166
175
  | StormEventScreenCandidate
167
176
  | StormEventFile
168
- | StormEventDone;
177
+ | StormEventDone
178
+ | StormEventDefinitionChange;
@@ -11,7 +11,7 @@ import { KapetaBodyRequest } from '../types';
11
11
  import { StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
12
12
  import { stormClient } from './stormClient';
13
13
  import { StormEvent } from './events';
14
- import { resolveOptions, StormEventParser } from './event-parser';
14
+ import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
15
15
  import { StormCodegen } from './codegen';
16
16
 
17
17
  const router = Router();
@@ -33,24 +33,29 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
33
33
  res.set('Content-Type', 'application/x-ndjson');
34
34
 
35
35
  metaStream.on('data', (data: StormEvent) => {
36
- eventParser.addEvent(data);
36
+ const result = eventParser.addEvent(req.params.handle, data);
37
+
38
+ sendDefinitions(res, result);
37
39
  });
38
40
 
39
41
  await streamStormPartialResponse(metaStream, res);
40
42
 
41
43
  if (!eventParser.isValid()) {
42
44
  // We can't continue if the meta stream is invalid
43
- res.write({
45
+ sendEvent(res, {
44
46
  type: 'ERROR_INTERNAL',
45
47
  payload: { error: eventParser.getError() },
46
48
  reason: 'Failed to generate system',
47
49
  created: Date.now(),
48
- } satisfies StormEvent);
50
+ });
49
51
  res.end();
50
52
  return;
51
53
  }
54
+
52
55
  const result = eventParser.toResult(handle);
53
56
 
57
+ sendDefinitions(res, result);
58
+
54
59
  const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
55
60
 
56
61
  const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
@@ -65,13 +70,20 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
65
70
  }
66
71
  });
67
72
 
73
+ function sendDefinitions(res: Response, result: StormDefinitions) {
74
+ sendEvent(res, {
75
+ type: 'DEFINITION_CHANGE',
76
+ payload: result,
77
+ reason: 'Updates to definition',
78
+ created: Date.now(),
79
+ });
80
+ }
81
+
68
82
  function sendDone(res: Response) {
69
- res.write(
70
- JSON.stringify({
71
- type: 'DONE',
72
- created: Date.now(),
73
- } satisfies StormEvent) + '\n'
74
- );
83
+ sendEvent(res, {
84
+ type: 'DONE',
85
+ created: Date.now(),
86
+ });
75
87
 
76
88
  res.end();
77
89
  }
@@ -79,14 +91,12 @@ function sendDone(res: Response) {
79
91
  function sendError(err: Error, res: Response) {
80
92
  console.error('Failed to send prompt', err);
81
93
  if (res.headersSent) {
82
- res.write(
83
- JSON.stringify({
84
- type: 'ERROR_INTERNAL',
85
- created: Date.now(),
86
- payload: { error: err.message },
87
- reason: 'Failed while sending prompt',
88
- } satisfies StormEvent) + '\n'
89
- );
94
+ sendEvent(res, {
95
+ type: 'ERROR_INTERNAL',
96
+ created: Date.now(),
97
+ payload: { error: err.message },
98
+ reason: 'Failed while sending prompt',
99
+ });
90
100
  } else {
91
101
  res.status(400).send({ error: err.message });
92
102
  }
@@ -95,7 +105,7 @@ function sendError(err: Error, res: Response) {
95
105
  function streamStormPartialResponse(result: StormStream, res: Response) {
96
106
  return new Promise<void>((resolve, reject) => {
97
107
  result.on('data', (data) => {
98
- res.write(JSON.stringify(data) + '\n');
108
+ sendEvent(res, data);
99
109
  });
100
110
 
101
111
  result.on('error', (err) => {
@@ -108,4 +118,8 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
108
118
  });
109
119
  }
110
120
 
121
+ function sendEvent(res: Response, evt: StormEvent) {
122
+ res.write(JSON.stringify(evt) + '\n');
123
+ }
124
+
111
125
  export default router;
@@ -34,6 +34,10 @@ class StormClient {
34
34
  //headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
35
35
  }
36
36
 
37
+ if (body.conversationId) {
38
+ headers['conversationId'] = body.conversationId;
39
+ }
40
+
37
41
  return {
38
42
  url,
39
43
  method: method,
@@ -74,6 +74,7 @@ export interface ConversationItem {
74
74
 
75
75
  export interface StormContextRequest<T = string> {
76
76
  history?: ConversationItem[];
77
+ conversationId?: string;
77
78
  prompt: T;
78
79
  }
79
80