@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 +1 -0
- package/CHANGELOG.md +17 -0
- package/dist/cjs/src/storm/codegen.js +68 -22
- package/dist/cjs/src/storm/event-parser.js +26 -11
- package/dist/cjs/src/storm/events.d.ts +1 -0
- package/dist/cjs/src/storm/stormClient.d.ts +2 -1
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/cjs/src/storm/stream.d.ts +8 -0
- package/dist/cjs/test/storm/event-parser.test.js +18 -0
- package/dist/cjs/test/storm/simple-blog-events.json +470 -0
- package/dist/esm/src/storm/codegen.js +68 -22
- package/dist/esm/src/storm/event-parser.js +26 -11
- package/dist/esm/src/storm/events.d.ts +1 -0
- package/dist/esm/src/storm/stormClient.d.ts +2 -1
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/dist/esm/src/storm/stream.d.ts +8 -0
- package/dist/esm/test/storm/event-parser.test.js +18 -0
- package/dist/esm/test/storm/simple-blog-events.json +470 -0
- package/package.json +2 -2
- package/src/storm/codegen.ts +101 -39
- package/src/storm/event-parser.ts +30 -13
- package/src/storm/events.ts +1 -0
- package/src/storm/stormClient.ts +9 -1
- package/src/storm/stream.ts +9 -0
- package/test/storm/event-parser.test.ts +19 -0
- package/test/storm/simple-blog-events.json +470 -0
- package/tsconfig.json +2 -1
package/src/storm/codegen.ts
CHANGED
@@ -13,6 +13,7 @@ import {
|
|
13
13
|
GeneratedResult,
|
14
14
|
MODE_CREATE_ONLY,
|
15
15
|
} from '@kapeta/codegen';
|
16
|
+
|
16
17
|
import { BlockDefinition } from '@kapeta/schemas';
|
17
18
|
import { codeGeneratorManager } from '../codeGeneratorManager';
|
18
19
|
import { STORM_ID, stormClient } from './stormClient';
|
@@ -23,6 +24,7 @@ import {
|
|
23
24
|
StormEventFileChunk,
|
24
25
|
StormEventFileDone,
|
25
26
|
StormEventFileLogical,
|
27
|
+
StormEventScreen,
|
26
28
|
} from './events';
|
27
29
|
import { BlockDefinitionInfo, StormEventParser } from './event-parser';
|
28
30
|
import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
|
@@ -31,11 +33,14 @@ import { writeFile } from 'fs/promises';
|
|
31
33
|
import path from 'path';
|
32
34
|
import Path, { join } from 'path';
|
33
35
|
import os from 'node:os';
|
34
|
-
import { readFileSync, writeFileSync } from 'fs';
|
36
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
35
37
|
import YAML from 'yaml';
|
36
|
-
import
|
38
|
+
import assert from 'assert';
|
37
39
|
|
38
|
-
type ImplementationGenerator =
|
40
|
+
type ImplementationGenerator<T = StormFileImplementationPrompt> = (
|
41
|
+
prompt: T,
|
42
|
+
conversationId?: string
|
43
|
+
) => Promise<StormStream>;
|
39
44
|
|
40
45
|
interface ErrorClassification {
|
41
46
|
error: string;
|
@@ -266,9 +271,13 @@ export class StormCodegen {
|
|
266
271
|
const blockUri = parseKapetaUri(block.uri);
|
267
272
|
|
268
273
|
const relevantFiles: StormFileInfo[] = allFiles.filter(
|
269
|
-
(file) =>
|
274
|
+
(file) =>
|
275
|
+
file.type !== AIFileTypes.IGNORE &&
|
276
|
+
file.type !== AIFileTypes.WEB_SCREEN &&
|
277
|
+
file.type !== AIFileTypes.WEB_ROUTER
|
270
278
|
);
|
271
|
-
const uiTemplates
|
279
|
+
const uiTemplates = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
|
280
|
+
const webRouters = allFiles.filter((file) => file.type === AIFileTypes.WEB_ROUTER);
|
272
281
|
const screenFiles: StormEventFileDone[] = [];
|
273
282
|
let filteredEvents = [] as StormEvent[];
|
274
283
|
for (const event of this.events) {
|
@@ -277,36 +286,96 @@ export class StormCodegen {
|
|
277
286
|
filteredEvents = [];
|
278
287
|
}
|
279
288
|
}
|
280
|
-
if (uiTemplates.length > 0) {
|
281
|
-
const uiStream = await stormClient.createUIImplementation({
|
282
|
-
events: filteredEvents,
|
283
|
-
templates: uiTemplates,
|
284
|
-
context: relevantFiles,
|
285
|
-
blockName: block.aiName,
|
286
|
-
prompt: this.userPrompt,
|
287
|
-
});
|
288
289
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
290
|
+
const screenEvents: StormEventScreen[] = [];
|
291
|
+
// generate screens
|
292
|
+
const screenStream = await stormClient.listScreens({
|
293
|
+
events: filteredEvents,
|
294
|
+
templates: uiTemplates,
|
295
|
+
context: relevantFiles,
|
296
|
+
blockName: block.aiName,
|
297
|
+
prompt: this.userPrompt,
|
298
|
+
});
|
299
|
+
screenStream.on('data', (evt) => {
|
300
|
+
if (evt.type === 'SCREEN') {
|
301
|
+
screenEvents.push(evt);
|
302
|
+
}
|
303
|
+
this.handleUiOutput(blockUri, block.aiName, evt);
|
304
|
+
});
|
295
305
|
|
296
|
-
|
297
|
-
|
298
|
-
|
306
|
+
this.out.on('aborted', () => {
|
307
|
+
screenStream.abort();
|
308
|
+
});
|
299
309
|
|
300
|
-
|
301
|
-
|
310
|
+
await screenStream.waitForDone();
|
311
|
+
|
312
|
+
// screenfiles
|
313
|
+
const screenTemplates = screenEvents
|
314
|
+
.map((screenEvent) => ({
|
315
|
+
...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
|
316
|
+
filename: screenEvent.payload.filename,
|
317
|
+
}))
|
318
|
+
.filter((tpl): tpl is StormFileInfo => !!tpl.content);
|
319
|
+
|
320
|
+
await Promise.all(
|
321
|
+
screenTemplates.concat(webRouters).map(async (template) => {
|
322
|
+
const payload = {
|
323
|
+
events: filteredEvents,
|
324
|
+
blockName: block.aiName,
|
325
|
+
filename: template.filename,
|
326
|
+
template: template,
|
327
|
+
context: relevantFiles.concat([
|
328
|
+
{
|
329
|
+
type: AIFileTypes.INSTRUCTIONS,
|
330
|
+
mode: MODE_CREATE_ONLY,
|
331
|
+
permissions: '0644',
|
332
|
+
filename: '<screens>.md',
|
333
|
+
content: `
|
334
|
+
# Generated screens
|
335
|
+
|
336
|
+
${JSON.stringify({ screenEvents })}
|
337
|
+
|
338
|
+
`,
|
339
|
+
},
|
340
|
+
]),
|
341
|
+
prompt: this.userPrompt,
|
342
|
+
};
|
343
|
+
|
344
|
+
const uiStream = await stormClient.createUIImplementation(payload);
|
345
|
+
|
346
|
+
uiStream.on('data', (evt) => {
|
347
|
+
const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
|
348
|
+
if (uiFile != undefined) {
|
349
|
+
screenFiles.push(uiFile);
|
350
|
+
}
|
351
|
+
});
|
352
|
+
|
353
|
+
this.out.on('aborted', () => {
|
354
|
+
uiStream.abort();
|
355
|
+
});
|
356
|
+
|
357
|
+
await uiStream.waitForDone();
|
358
|
+
})
|
359
|
+
);
|
302
360
|
|
303
361
|
if (this.isAborted()) {
|
304
362
|
return;
|
305
363
|
}
|
364
|
+
const basePath = this.getBasePath(block.content.metadata.name);
|
365
|
+
|
366
|
+
const screenFilesConverted = screenFiles.map((screenFile) => {
|
367
|
+
return {
|
368
|
+
filename: screenFile.payload.filename,
|
369
|
+
content: screenFile.payload.content,
|
370
|
+
mode: MODE_CREATE_ONLY,
|
371
|
+
permissions: '0644',
|
372
|
+
type: AIFileTypes.WEB_SCREEN,
|
373
|
+
};
|
374
|
+
});
|
306
375
|
|
307
376
|
// Gather the context files for implementation. These will be all be passed to the AI
|
308
377
|
const contextFiles: StormFileInfo[] = relevantFiles.filter(
|
309
|
-
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
|
378
|
+
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN, AIFileTypes.WEB_ROUTER].includes(file.type)
|
310
379
|
);
|
311
380
|
|
312
381
|
// Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
|
@@ -321,12 +390,15 @@ export class StormCodegen {
|
|
321
390
|
);
|
322
391
|
}
|
323
392
|
|
324
|
-
const basePath = this.getBasePath(block.content.metadata.name);
|
325
|
-
|
326
393
|
if (this.isAborted()) {
|
327
394
|
return;
|
328
395
|
}
|
329
396
|
|
397
|
+
for (const screenFile of screenFilesConverted) {
|
398
|
+
const filePath = join(basePath, screenFile.filename);
|
399
|
+
await writeFile(filePath, screenFile.content);
|
400
|
+
}
|
401
|
+
|
330
402
|
for (const serviceFile of serviceFiles) {
|
331
403
|
const filePath = join(basePath, serviceFile.filename);
|
332
404
|
await writeFile(filePath, serviceFile.content);
|
@@ -345,16 +417,6 @@ export class StormCodegen {
|
|
345
417
|
await writeFile(filePath, screenFile.payload.content);
|
346
418
|
}
|
347
419
|
|
348
|
-
const screenFilesConverted = screenFiles.map((screenFile) => {
|
349
|
-
return {
|
350
|
-
filename: screenFile.payload.filename,
|
351
|
-
content: screenFile.payload.content,
|
352
|
-
mode: MODE_CREATE_ONLY,
|
353
|
-
permissions: '0644',
|
354
|
-
type: AIFileTypes.WEB_SCREEN,
|
355
|
-
};
|
356
|
-
});
|
357
|
-
allFiles.push(...screenFilesConverted);
|
358
420
|
const blockRef = block.uri;
|
359
421
|
|
360
422
|
this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.QA);
|
@@ -594,7 +656,7 @@ export class StormCodegen {
|
|
594
656
|
files.add(filename);
|
595
657
|
|
596
658
|
const requestedFiles = Array.from(files).flatMap((file) => {
|
597
|
-
if (
|
659
|
+
if (existsSync(file)) {
|
598
660
|
return file;
|
599
661
|
}
|
600
662
|
|
@@ -696,7 +758,7 @@ export class StormCodegen {
|
|
696
758
|
return;
|
697
759
|
}
|
698
760
|
|
699
|
-
if (
|
761
|
+
if ([AIFileTypes.WEB_ROUTER, AIFileTypes.WEB_SCREEN].includes(file.type)) {
|
700
762
|
// Don't send the web screen files to the stream yet
|
701
763
|
// They will need to be implemented by the AI
|
702
764
|
return;
|
@@ -26,7 +26,7 @@ import {
|
|
26
26
|
DSLAPIParser,
|
27
27
|
DSLController,
|
28
28
|
DSLConverters,
|
29
|
-
DSLDataTypeParser,
|
29
|
+
DSLDataTypeParser, DSLEntityType,
|
30
30
|
DSLMethod,
|
31
31
|
DSLParser,
|
32
32
|
KAPLANG_ID,
|
@@ -325,13 +325,13 @@ export class StormEventParser {
|
|
325
325
|
this.connections.push(evt.payload);
|
326
326
|
break;
|
327
327
|
case 'API_RETRY':
|
328
|
-
Object.values(this.blocks).forEach(block => {
|
328
|
+
Object.values(this.blocks).forEach((block) => {
|
329
329
|
block.types = [];
|
330
330
|
block.apis = [];
|
331
331
|
});
|
332
332
|
break;
|
333
333
|
case 'MODEL_RETRY':
|
334
|
-
Object.values(this.blocks).forEach(block => {
|
334
|
+
Object.values(this.blocks).forEach((block) => {
|
335
335
|
block.models = [];
|
336
336
|
});
|
337
337
|
break;
|
@@ -537,7 +537,7 @@ export class StormEventParser {
|
|
537
537
|
|
538
538
|
const blockSpec = blockDefinitionInfo.content.spec;
|
539
539
|
|
540
|
-
|
540
|
+
const apiResources: { [key: string]: Resource | undefined } = {};
|
541
541
|
let dbResource: Resource | undefined = undefined;
|
542
542
|
|
543
543
|
blockInfo.resources.forEach((resource) => {
|
@@ -546,10 +546,7 @@ export class StormEventParser {
|
|
546
546
|
};
|
547
547
|
switch (resource.type) {
|
548
548
|
case 'API':
|
549
|
-
|
550
|
-
break;
|
551
|
-
}
|
552
|
-
apiResource = {
|
549
|
+
const apiResource = {
|
553
550
|
kind: this.toResourceKind(resource.type),
|
554
551
|
metadata: {
|
555
552
|
name: resource.name,
|
@@ -565,6 +562,7 @@ export class StormEventParser {
|
|
565
562
|
} satisfies SourceCode,
|
566
563
|
},
|
567
564
|
};
|
565
|
+
apiResources[resource.name] = apiResource;
|
568
566
|
blockSpec.providers!.push(apiResource);
|
569
567
|
break;
|
570
568
|
case 'CLIENT':
|
@@ -646,11 +644,30 @@ export class StormEventParser {
|
|
646
644
|
}
|
647
645
|
});
|
648
646
|
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
});
|
653
|
-
|
647
|
+
blockInfo.apis.forEach((api) => {
|
648
|
+
const dslApi = DSLAPIParser.parse(api, {
|
649
|
+
ignoreSemantics: true,
|
650
|
+
}) as (DSLMethod | DSLController)[];
|
651
|
+
|
652
|
+
let exactMatch = false;
|
653
|
+
if (dslApi[0] && dslApi[0].type == DSLEntityType.CONTROLLER) {
|
654
|
+
const name = dslApi[0].name.toLowerCase();
|
655
|
+
const apiResourceName = Object.keys(apiResources).find((key) => key.indexOf(name) > -1);
|
656
|
+
if (apiResourceName) {
|
657
|
+
const exactResource = apiResources[apiResourceName];
|
658
|
+
exactResource!.spec.source.value += api + '\n\n';
|
659
|
+
exactMatch = true;
|
660
|
+
}
|
661
|
+
}
|
662
|
+
|
663
|
+
if (!exactMatch) {
|
664
|
+
// if we couldn't place the given api on the exact resource we just park it on the first
|
665
|
+
// available rest resource
|
666
|
+
const firstKey = Object.keys(apiResources)[0];
|
667
|
+
const firstEntry = apiResources[firstKey];
|
668
|
+
firstEntry!.spec.source.value += api + '\n\n';
|
669
|
+
}
|
670
|
+
});
|
654
671
|
|
655
672
|
blockInfo.types.forEach((type) => {
|
656
673
|
blockSpec.entities!.source!.value += type + '\n';
|
package/src/storm/events.ts
CHANGED
package/src/storm/stormClient.ts
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
StormFileImplementationPrompt,
|
13
13
|
StormStream,
|
14
14
|
StormUIImplementationPrompt,
|
15
|
+
StormUIListPrompt,
|
15
16
|
} from './stream';
|
16
17
|
import { getRawAsset } from 'node:sea';
|
17
18
|
|
@@ -112,8 +113,15 @@ class StormClient {
|
|
112
113
|
});
|
113
114
|
}
|
114
115
|
|
116
|
+
public listScreens(prompt: StormUIListPrompt, conversationId?: string) {
|
117
|
+
return this.send('/v2/ui/list', {
|
118
|
+
prompt,
|
119
|
+
conversationId,
|
120
|
+
});
|
121
|
+
}
|
122
|
+
|
115
123
|
public createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string) {
|
116
|
-
return this.send
|
124
|
+
return this.send('/v2/ui/merge', {
|
117
125
|
prompt,
|
118
126
|
conversationId,
|
119
127
|
});
|
package/src/storm/stream.ts
CHANGED
@@ -117,6 +117,15 @@ export interface StormFileImplementationPrompt {
|
|
117
117
|
}
|
118
118
|
|
119
119
|
export interface StormUIImplementationPrompt {
|
120
|
+
events: StormEvent[];
|
121
|
+
template: StormFileInfo;
|
122
|
+
filename: string;
|
123
|
+
context: StormFileInfo[];
|
124
|
+
blockName: string;
|
125
|
+
prompt: string;
|
126
|
+
}
|
127
|
+
|
128
|
+
export interface StormUIListPrompt {
|
120
129
|
events: StormEvent[];
|
121
130
|
templates: StormFileInfo[];
|
122
131
|
context: StormFileInfo[];
|
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
import { StormEventParser } from '../../src/storm/event-parser';
|
7
7
|
import { StormEvent } from '../../src/storm/events';
|
8
|
+
import simpleBlogEvents from './simple-blog-events.json';
|
8
9
|
|
9
10
|
const parserOptions = {
|
10
11
|
serviceKind: 'kapeta/block-service:local',
|
@@ -179,4 +180,22 @@ describe('event-parser', () => {
|
|
179
180
|
expect(result.plan.spec.connections[0].provider.blockId).toBe(serviceBlockInstance.id);
|
180
181
|
expect(result.plan.spec.connections[0].provider.resourceName).toBe(apiResource?.metadata.name);
|
181
182
|
});
|
183
|
+
|
184
|
+
it('it will split api into correct provider', () => {
|
185
|
+
const events = simpleBlogEvents as StormEvent[];
|
186
|
+
const parser = new StormEventParser(parserOptions);
|
187
|
+
events.forEach((event) => parser.processEvent('kapeta', event));
|
188
|
+
|
189
|
+
const result = parser.toResult('kapeta');
|
190
|
+
|
191
|
+
const blogService = result.blocks.find((block) => block.aiName === 'blog-service');
|
192
|
+
expect(blogService).toBeDefined();
|
193
|
+
expect(blogService?.content).toBeDefined();
|
194
|
+
|
195
|
+
const apiProviders = blogService?.content?.spec?.providers?.filter((provider) => provider.kind === 'kapeta/block-type-api:local');
|
196
|
+
expect(apiProviders).toBeDefined();
|
197
|
+
expect(apiProviders!.length).toBe(2);
|
198
|
+
expect(apiProviders!["0"].spec.source.value).not.toBe('');
|
199
|
+
expect(apiProviders!["1"].spec.source.value).not.toBe('');
|
200
|
+
});
|
182
201
|
});
|