@kapeta/local-cluster-service 0.55.2 → 0.56.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 +20 -0
- package/definitions.d.ts +1 -1
- package/dist/cjs/src/storm/codegen.d.ts +2 -1
- package/dist/cjs/src/storm/codegen.js +4 -1
- package/dist/cjs/src/storm/event-parser.d.ts +2 -1
- package/dist/cjs/src/storm/event-parser.js +37 -9
- package/dist/cjs/src/storm/routes.js +26 -7
- package/dist/cjs/src/storm/stream.d.ts +7 -0
- package/dist/cjs/test/storm/blog-events.json +860 -0
- package/dist/cjs/test/storm/codegen.test.js +2 -2
- package/dist/cjs/test/storm/event-parser.test.js +25 -7
- package/dist/cjs/test/storm/predefined-user-events.json +9 -0
- package/dist/esm/src/storm/codegen.d.ts +2 -1
- package/dist/esm/src/storm/codegen.js +4 -1
- package/dist/esm/src/storm/event-parser.d.ts +2 -1
- package/dist/esm/src/storm/event-parser.js +37 -9
- package/dist/esm/src/storm/routes.js +26 -7
- package/dist/esm/src/storm/stream.d.ts +7 -0
- package/dist/esm/test/storm/blog-events.json +860 -0
- package/dist/esm/test/storm/codegen.test.js +2 -2
- package/dist/esm/test/storm/event-parser.test.js +25 -7
- package/dist/esm/test/storm/predefined-user-events.json +9 -0
- package/package.json +1 -1
- package/src/storm/codegen.ts +7 -2
- package/src/storm/event-parser.ts +60 -25
- package/src/storm/routes.ts +32 -9
- package/src/storm/stream.ts +8 -0
- package/test/storm/blog-events.json +860 -0
- package/test/storm/codegen.test.ts +4 -2
- package/test/storm/event-parser.test.ts +27 -7
- package/test/storm/predefined-user-events.json +9 -0
@@ -26,8 +26,8 @@ describe('codegen', () => {
|
|
26
26
|
await codegen.process();
|
27
27
|
const stormEvents = await codegenPromise;
|
28
28
|
expect(stormEvents.filter((event) => event.type === 'FILE_DONE').length).toBeGreaterThan(1);
|
29
|
-
expect(stormEvents.filter((event) => event.type === 'FILE_DONE' && event.payload.filename.endsWith('UserDTO.java'))
|
30
|
-
.toBe(1);
|
29
|
+
expect(stormEvents.filter((event) => event.type === 'FILE_DONE' && event.payload.filename.endsWith('UserDTO.java'))
|
30
|
+
.length).toBe(1);
|
31
31
|
expect(stormEvents.at(-1).type).toBe('BLOCK_READY');
|
32
32
|
});
|
33
33
|
});
|
@@ -11,9 +11,10 @@ exports.parserOptions = void 0;
|
|
11
11
|
const event_parser_1 = require("../../src/storm/event-parser");
|
12
12
|
const simple_blog_events_json_1 = __importDefault(require("./simple-blog-events.json"));
|
13
13
|
const predefined_user_events_json_1 = __importDefault(require("./predefined-user-events.json"));
|
14
|
+
const blog_events_json_1 = __importDefault(require("./blog-events.json"));
|
14
15
|
exports.parserOptions = {
|
15
16
|
serviceKind: 'kapeta/block-service:local',
|
16
|
-
serviceLanguage: 'kapeta/language-target-
|
17
|
+
serviceLanguage: 'kapeta/language-target-java-spring-boot:local',
|
17
18
|
frontendKind: 'kapeta/block-type-frontend:local',
|
18
19
|
frontendLanguage: 'kapeta/language-target-react-ts:local',
|
19
20
|
cliKind: 'kapeta/block-type-cli:local',
|
@@ -27,7 +28,7 @@ exports.parserOptions = {
|
|
27
28
|
publisherKind: 'kapeta/resource-type-publisher:local',
|
28
29
|
subscriberKind: 'kapeta/resource-type-subscriber:local',
|
29
30
|
databaseKind: 'kapeta/block-type-database:local',
|
30
|
-
apiKind: 'kapeta/
|
31
|
+
apiKind: 'kapeta/resource-type-rest-api:local',
|
31
32
|
clientKind: 'kapeta/block-type-client:local',
|
32
33
|
webPageKind: 'kapeta/block-type-web-page:local',
|
33
34
|
webFragmentKind: 'kapeta/block-type-web-fragment:local',
|
@@ -176,7 +177,7 @@ describe('event-parser', () => {
|
|
176
177
|
const blogService = result.blocks.find((block) => block.aiName === 'blog-service');
|
177
178
|
expect(blogService).toBeDefined();
|
178
179
|
expect(blogService?.content).toBeDefined();
|
179
|
-
const apiProviders = blogService?.content?.spec?.providers?.filter((provider) => provider.kind === 'kapeta/
|
180
|
+
const apiProviders = blogService?.content?.spec?.providers?.filter((provider) => provider.kind === 'kapeta/resource-type-rest-api:local');
|
180
181
|
expect(apiProviders).toBeDefined();
|
181
182
|
expect(apiProviders.length).toBe(2);
|
182
183
|
expect(apiProviders['0'].spec.source.value).not.toBe('');
|
@@ -190,9 +191,26 @@ describe('event-parser', () => {
|
|
190
191
|
}
|
191
192
|
const result = await parser.toResult('kapeta');
|
192
193
|
expect(result.blocks.length).toBe(1);
|
193
|
-
|
194
|
-
expect(
|
195
|
-
expect(
|
196
|
-
expect(
|
194
|
+
const block = result.blocks[0];
|
195
|
+
expect(block.content.metadata.title).toBe('user-service');
|
196
|
+
expect(block.content.metadata.title).toBe('user-service');
|
197
|
+
expect(block.content.metadata.name).toBe('kapeta/user-service');
|
198
|
+
expect(block.archetype).toBeDefined();
|
199
|
+
expect(block.content.spec.target?.options).toBeDefined();
|
200
|
+
expect(block.content.spec.target?.options.groupId).toBe('ai.kapeta');
|
201
|
+
expect(block.content.spec.target?.options.artifactId).toBe('blogplatform');
|
202
|
+
expect(block.content.spec.target?.options.basePackage).toBe('ai.kapeta.blogplatform');
|
203
|
+
});
|
204
|
+
it('ensure post-service has api', async () => {
|
205
|
+
const events = blog_events_json_1.default;
|
206
|
+
const parser = new event_parser_1.StormEventParser(exports.parserOptions);
|
207
|
+
for (const event of events) {
|
208
|
+
await parser.processEvent('kapeta', event);
|
209
|
+
}
|
210
|
+
const result = await parser.toResult('kapeta', true);
|
211
|
+
const postService = result.blocks.find((block) => block.aiName === 'post-service');
|
212
|
+
expect(postService).toBeDefined();
|
213
|
+
expect(postService?.content?.spec?.providers?.length).toBe(1);
|
214
|
+
expect(postService?.content?.spec?.providers[0].spec.source.value.startsWith('controller')).toBeTruthy();
|
197
215
|
});
|
198
216
|
});
|
@@ -1,4 +1,13 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"type": "CREATE_PLAN_PROPERTIES",
|
4
|
+
"reason": "Define the properties for the plan itself",
|
5
|
+
"payload": {
|
6
|
+
"description": "A blogging platform with user management, post management, and comment management functionalities.",
|
7
|
+
"name": "Blog Platform"
|
8
|
+
},
|
9
|
+
"created": 1720768973436
|
10
|
+
},
|
2
11
|
{
|
3
12
|
"type": "CREATE_BLOCK",
|
4
13
|
"reason": "Handles user registration, authentication, and authorization.",
|
package/package.json
CHANGED
package/src/storm/codegen.ts
CHANGED
@@ -106,7 +106,7 @@ export class StormCodegen {
|
|
106
106
|
private readonly blocks: BlockDefinitionInfo[];
|
107
107
|
private readonly out = new StormStream();
|
108
108
|
private readonly events: StormEvent[];
|
109
|
-
private
|
109
|
+
private tmpDir: string;
|
110
110
|
private readonly conversationId: string;
|
111
111
|
|
112
112
|
constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
@@ -117,6 +117,10 @@ export class StormCodegen {
|
|
117
117
|
this.conversationId = conversationId;
|
118
118
|
}
|
119
119
|
|
120
|
+
public setTmpDir(tmpDir: string) {
|
121
|
+
this.tmpDir = tmpDir;
|
122
|
+
}
|
123
|
+
|
120
124
|
public async process() {
|
121
125
|
const promises = this.blocks.map((block) => {
|
122
126
|
if (block.archetype) {
|
@@ -273,7 +277,7 @@ export class StormCodegen {
|
|
273
277
|
payload: {
|
274
278
|
filename: data.payload.filename,
|
275
279
|
path: join(this.getBasePath(blockUri.fullName), data.payload.filename),
|
276
|
-
content: data.payload.content,
|
280
|
+
content: data.payload.content.replace(/\\n/g, '\n'),
|
277
281
|
blockRef: ref,
|
278
282
|
instanceId: StormEventParser.toInstanceIdFromRef(ref),
|
279
283
|
},
|
@@ -320,6 +324,7 @@ export class StormCodegen {
|
|
320
324
|
file.type !== AIFileTypes.WEB_ROUTER
|
321
325
|
);
|
322
326
|
const uiTemplates = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
|
327
|
+
|
323
328
|
const screenFiles: StormEventFileDone[] = [];
|
324
329
|
|
325
330
|
let filteredEvents = [] as StormEvent[];
|
@@ -76,6 +76,7 @@ export interface StormOptions {
|
|
76
76
|
desktopKind: string;
|
77
77
|
desktopLanguage: string;
|
78
78
|
gatewayKind: string;
|
79
|
+
[key: string]: string;
|
79
80
|
}
|
80
81
|
|
81
82
|
function prettifyKaplang(source: string) {
|
@@ -288,7 +289,7 @@ export class StormEventParser {
|
|
288
289
|
this.blocks[evt.payload.name] = {
|
289
290
|
...evt.payload,
|
290
291
|
apis: apis ?? [],
|
291
|
-
models: models ??[],
|
292
|
+
models: models ?? [],
|
292
293
|
types: types ?? [],
|
293
294
|
};
|
294
295
|
evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.name).toNormalizedString();
|
@@ -364,7 +365,7 @@ export class StormEventParser {
|
|
364
365
|
return this.error;
|
365
366
|
}
|
366
367
|
|
367
|
-
public async toResult(handle: string): Promise<StormDefinitions> {
|
368
|
+
public async toResult(handle: string, warn: boolean = false): Promise<StormDefinitions> {
|
368
369
|
const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
|
369
370
|
const blockDefinitions = await this.toBlockDefinitions(handle);
|
370
371
|
const refIdMap: { [key: string]: string } = {};
|
@@ -409,12 +410,14 @@ export class StormEventParser {
|
|
409
410
|
);
|
410
411
|
|
411
412
|
if (!apiResource) {
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
413
|
+
if (warn) {
|
414
|
+
console.warn(
|
415
|
+
'API resource not found: %s on %s',
|
416
|
+
apiConnection.fromResource,
|
417
|
+
apiProviderRef.toNormalizedString(),
|
418
|
+
apiConnection
|
419
|
+
);
|
420
|
+
}
|
418
421
|
return;
|
419
422
|
}
|
420
423
|
|
@@ -428,12 +431,14 @@ export class StormEventParser {
|
|
428
431
|
});
|
429
432
|
|
430
433
|
if (!clientResource) {
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
434
|
+
if (warn) {
|
435
|
+
console.warn(
|
436
|
+
'Client resource not found: %s on %s',
|
437
|
+
apiConnection.toResource,
|
438
|
+
clientConsumerRef.toNormalizedString(),
|
439
|
+
apiConnection
|
440
|
+
);
|
441
|
+
}
|
437
442
|
return;
|
438
443
|
}
|
439
444
|
|
@@ -487,7 +492,7 @@ export class StormEventParser {
|
|
487
492
|
blockId: refIdMap[fromRef.toNormalizedString()],
|
488
493
|
resourceName: connection.fromResource,
|
489
494
|
},
|
490
|
-
mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
|
495
|
+
mapping: this.toConnectionMapping(handle, connection, blockDefinitions, warn),
|
491
496
|
} satisfies Connection;
|
492
497
|
});
|
493
498
|
|
@@ -521,7 +526,7 @@ export class StormEventParser {
|
|
521
526
|
let blockDefinitionInfo: BlockDefinitionInfo;
|
522
527
|
|
523
528
|
if (blockInfo.archetype) {
|
524
|
-
blockDefinitionInfo = await this.resolveArchetypeBlockDefinition(blockRef, blockInfo);
|
529
|
+
blockDefinitionInfo = await this.resolveArchetypeBlockDefinition(blockRef, blockInfo, handle);
|
525
530
|
} else {
|
526
531
|
blockDefinitionInfo = this.createBlockDefinitionInfo(blockRef, blockInfo, handle);
|
527
532
|
}
|
@@ -697,7 +702,7 @@ export class StormEventParser {
|
|
697
702
|
if (firstEntry) {
|
698
703
|
firstEntry.spec.source.value += api + '\n\n';
|
699
704
|
} else {
|
700
|
-
|
705
|
+
// this might be ok as we might receive api and types before resources from the ai-service
|
701
706
|
}
|
702
707
|
}
|
703
708
|
});
|
@@ -770,7 +775,8 @@ export class StormEventParser {
|
|
770
775
|
private toConnectionMapping(
|
771
776
|
handle: string,
|
772
777
|
connection: StormConnection,
|
773
|
-
blockDefinitions: { [key: string]: BlockDefinitionInfo }
|
778
|
+
blockDefinitions: { [key: string]: BlockDefinitionInfo },
|
779
|
+
warn: boolean
|
774
780
|
): any {
|
775
781
|
if (connection.fromResourceType !== 'API') {
|
776
782
|
return;
|
@@ -789,12 +795,14 @@ export class StormEventParser {
|
|
789
795
|
);
|
790
796
|
|
791
797
|
if (!apiResource) {
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
+
if (warn) {
|
799
|
+
console.warn(
|
800
|
+
'API resource not found: %s on %s',
|
801
|
+
connection.fromResource,
|
802
|
+
fromRef.toNormalizedString(),
|
803
|
+
connection
|
804
|
+
);
|
805
|
+
}
|
798
806
|
return;
|
799
807
|
}
|
800
808
|
|
@@ -888,16 +896,43 @@ export class StormEventParser {
|
|
888
896
|
|
889
897
|
private async resolveArchetypeBlockDefinition(
|
890
898
|
blockRef: KapetaURI,
|
891
|
-
blockInfo: StormBlockInfoFilled
|
899
|
+
blockInfo: StormBlockInfoFilled,
|
900
|
+
handle: string
|
892
901
|
): Promise<BlockDefinitionInfo> {
|
893
902
|
const predefinedBlock = PREDEFINED_BLOCKS.get(blockInfo.archetype!);
|
894
903
|
if (!predefinedBlock) {
|
895
904
|
throw new Error('Predefined block not found for archetype [' + blockInfo.archetype + ']');
|
896
905
|
}
|
897
906
|
|
907
|
+
const target = this.toBlockTarget(handle, blockInfo.type);
|
908
|
+
|
898
909
|
const blockDefinition = await predefinedBlock.getBlockDefinition();
|
899
910
|
_.set(blockDefinition!, ['metadata', 'name'], blockRef.fullName);
|
900
911
|
_.set(blockDefinition!, ['metadata', 'title'], blockRef.name);
|
912
|
+
_.set(blockDefinition!, ['spec', 'target', 'options'], target?.options);
|
913
|
+
|
914
|
+
const options: StormOptions = this.options;
|
915
|
+
|
916
|
+
function getKind(kind: string): string | undefined {
|
917
|
+
for (const prop in options) {
|
918
|
+
if (options.hasOwnProperty(prop)) {
|
919
|
+
const value = options[prop];
|
920
|
+
if (value.startsWith(kind)) {
|
921
|
+
return value;
|
922
|
+
}
|
923
|
+
}
|
924
|
+
}
|
925
|
+
}
|
926
|
+
|
927
|
+
blockDefinition!.kind = getKind(parseKapetaUri(blockDefinition!.kind).fullName) ?? blockDefinition!.kind;
|
928
|
+
for (const provider of blockDefinition!.spec.providers ?? []) {
|
929
|
+
provider.kind = getKind(parseKapetaUri(provider.kind).fullName) ?? provider.kind;
|
930
|
+
}
|
931
|
+
for (const consumer of blockDefinition!.spec.consumers ?? []) {
|
932
|
+
consumer.kind = getKind(parseKapetaUri(consumer.kind).fullName) ?? consumer.kind;
|
933
|
+
}
|
934
|
+
blockDefinition!.spec.target!.kind =
|
935
|
+
getKind(parseKapetaUri(blockDefinition!.spec.target!.kind).fullName) ?? blockDefinition!.spec.target!.kind;
|
901
936
|
|
902
937
|
return {
|
903
938
|
uri: blockRef.toNormalizedString(),
|
package/src/storm/routes.ts
CHANGED
@@ -6,10 +6,12 @@
|
|
6
6
|
import Router from 'express-promise-router';
|
7
7
|
import FS from 'fs-extra';
|
8
8
|
import { Response } from 'express';
|
9
|
+
import Path from 'path';
|
10
|
+
import _ from 'lodash';
|
9
11
|
import { corsHandler } from '../middleware/cors';
|
10
12
|
import { stringBody } from '../middleware/stringBody';
|
11
13
|
import { KapetaBodyRequest } from '../types';
|
12
|
-
import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
|
14
|
+
import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
|
13
15
|
import { ConversationIdHeader, stormClient } from './stormClient';
|
14
16
|
import { StormEvent, StormEventPhaseType } from './events';
|
15
17
|
import {
|
@@ -21,8 +23,6 @@ import {
|
|
21
23
|
} from './event-parser';
|
22
24
|
import { StormCodegen } from './codegen';
|
23
25
|
import { assetManager } from '../assetManager';
|
24
|
-
import Path from 'path';
|
25
|
-
import _ from 'lodash';
|
26
26
|
|
27
27
|
const router = Router();
|
28
28
|
|
@@ -103,12 +103,13 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
103
103
|
return;
|
104
104
|
}
|
105
105
|
|
106
|
-
const result = await eventParser.toResult(handle);
|
106
|
+
const result = await eventParser.toResult(handle, true);
|
107
107
|
|
108
108
|
if (metaStream.isAborted()) {
|
109
109
|
return;
|
110
110
|
}
|
111
|
-
|
111
|
+
// Cancel debounce, we don't need to send the plan again
|
112
|
+
sendUpdatedPlan.cancel();
|
112
113
|
sendDefinitions(res, result);
|
113
114
|
|
114
115
|
if (!req.query.skipCodegen) {
|
@@ -169,6 +170,31 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
|
|
169
170
|
}
|
170
171
|
});
|
171
172
|
|
173
|
+
router.post('/block/codegen', async (req: KapetaBodyRequest, res: Response) => {
|
174
|
+
const body: StormCodegenRequest = JSON.parse(req.stringBody ?? '{}');
|
175
|
+
console.log('Codegen request', body);
|
176
|
+
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
177
|
+
try {
|
178
|
+
const stormCodegen = new StormCodegen(conversationId ?? '', body.prompt, [body.block], body.events || []);
|
179
|
+
stormCodegen.setTmpDir(body.outDir);
|
180
|
+
|
181
|
+
onRequestAborted(req, res, () => {
|
182
|
+
stormCodegen.abort();
|
183
|
+
});
|
184
|
+
|
185
|
+
const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
|
186
|
+
|
187
|
+
await stormCodegen.process();
|
188
|
+
|
189
|
+
await codegenPromise;
|
190
|
+
|
191
|
+
sendDone(res);
|
192
|
+
} catch (err: any) {
|
193
|
+
console.error('Failed to generate code', err);
|
194
|
+
res.status(500).send({ error: err.message });
|
195
|
+
}
|
196
|
+
});
|
197
|
+
|
172
198
|
function sendDefinitions(res: Response, result: StormDefinitions) {
|
173
199
|
sendEvent(res, {
|
174
200
|
type: 'DEFINITION_CHANGE',
|
@@ -228,10 +254,7 @@ function sendEvent(res: Response, evt: StormEvent) {
|
|
228
254
|
}
|
229
255
|
|
230
256
|
function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () => void) {
|
231
|
-
req.on('close', () => {
|
232
|
-
onAborted();
|
233
|
-
});
|
234
|
-
res.on('close', () => {
|
257
|
+
req.socket.on('close', () => {
|
235
258
|
onAborted();
|
236
259
|
});
|
237
260
|
}
|
package/src/storm/stream.ts
CHANGED
@@ -6,6 +6,7 @@ import { EventEmitter } from 'node:events';
|
|
6
6
|
import { StormEvent } from './events';
|
7
7
|
import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
|
8
8
|
import { BlockDefinition } from '@kapeta/schemas';
|
9
|
+
import { BlockDefinitionInfo } from './event-parser';
|
9
10
|
|
10
11
|
export class StormStream extends EventEmitter {
|
11
12
|
private conversationId: string = '';
|
@@ -106,6 +107,13 @@ export interface StormCreateBlockRequest {
|
|
106
107
|
newPath: string;
|
107
108
|
}
|
108
109
|
|
110
|
+
export interface StormCodegenRequest {
|
111
|
+
block: BlockDefinitionInfo;
|
112
|
+
prompt: string;
|
113
|
+
events: StormEvent[];
|
114
|
+
outDir: string;
|
115
|
+
}
|
116
|
+
|
109
117
|
export interface StormFileInfo extends GeneratedFile {
|
110
118
|
type: AIFileTypes;
|
111
119
|
}
|