@kapeta/local-cluster-service 0.47.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.
- package/CHANGELOG.md +7 -0
- package/dist/cjs/src/storm/codegen.js +16 -15
- package/dist/cjs/src/storm/event-parser.d.ts +1 -1
- package/dist/cjs/src/storm/event-parser.js +60 -52
- package/dist/cjs/src/storm/events.d.ts +2 -1
- package/dist/cjs/src/storm/stormClient.js +3 -0
- package/dist/cjs/src/storm/stream.d.ts +1 -0
- package/dist/cjs/test/storm/event-parser.test.js +0 -8
- package/dist/esm/src/storm/codegen.js +16 -15
- package/dist/esm/src/storm/event-parser.d.ts +1 -1
- package/dist/esm/src/storm/event-parser.js +60 -52
- package/dist/esm/src/storm/events.d.ts +2 -1
- package/dist/esm/src/storm/stormClient.js +3 -0
- package/dist/esm/src/storm/stream.d.ts +1 -0
- package/dist/esm/test/storm/event-parser.test.js +0 -8
- package/package.json +3 -3
- package/src/storm/codegen.ts +23 -14
- package/src/storm/event-parser.ts +100 -62
- package/src/storm/events.ts +2 -1
- package/src/storm/routes.ts +12 -13
- package/src/storm/stormClient.ts +4 -0
- package/src/storm/stream.ts +1 -0
- package/test/storm/event-parser.test.ts +0 -8
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## [0.47.1](https://github.com/kapetacom/local-cluster-service/compare/v0.47.0...v0.47.1) (2024-05-30)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* Make UUIDs deterministic to allow cleaner updates ([#150](https://github.com/kapetacom/local-cluster-service/issues/150)) ([0e3bbec](https://github.com/kapetacom/local-cluster-service/commit/0e3bbecb00d52e2cc0f5e0d8adb9953f40e6e253))
|
7
|
+
|
1
8
|
# [0.47.0](https://github.com/kapetacom/local-cluster-service/compare/v0.46.0...v0.47.0) (2024-05-29)
|
2
9
|
|
3
10
|
|
@@ -28,15 +28,15 @@ class StormCodegen {
|
|
28
28
|
getStream() {
|
29
29
|
return this.out;
|
30
30
|
}
|
31
|
-
handleTemplateFileOutput(blockUri, template, data) {
|
31
|
+
handleTemplateFileOutput(blockUri, aiName, template, data) {
|
32
32
|
switch (data.type) {
|
33
33
|
case 'FILE':
|
34
34
|
template.filename = data.payload.filename;
|
35
35
|
template.content = data.payload.content;
|
36
|
-
return this.handleFileOutput(blockUri, data);
|
36
|
+
return this.handleFileOutput(blockUri, aiName, data);
|
37
37
|
}
|
38
38
|
}
|
39
|
-
handleUiOutput(blockUri, data) {
|
39
|
+
handleUiOutput(blockUri, aiName, data) {
|
40
40
|
switch (data.type) {
|
41
41
|
case 'SCREEN':
|
42
42
|
this.out.emit('data', {
|
@@ -45,17 +45,17 @@ class StormCodegen {
|
|
45
45
|
created: Date.now(),
|
46
46
|
payload: {
|
47
47
|
...data.payload,
|
48
|
-
blockName:
|
48
|
+
blockName: aiName,
|
49
49
|
},
|
50
50
|
});
|
51
51
|
case 'FILE':
|
52
|
-
return this.handleFileOutput(blockUri, data);
|
52
|
+
return this.handleFileOutput(blockUri, aiName, data);
|
53
53
|
}
|
54
54
|
}
|
55
|
-
handleFileOutput(blockUri, data) {
|
55
|
+
handleFileOutput(blockUri, aiName, data) {
|
56
56
|
switch (data.type) {
|
57
57
|
case 'FILE':
|
58
|
-
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
58
|
+
this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
|
59
59
|
return {
|
60
60
|
type: 'FILE',
|
61
61
|
created: Date.now(),
|
@@ -77,7 +77,7 @@ class StormCodegen {
|
|
77
77
|
}
|
78
78
|
const allFiles = this.toStormFiles(generatedResult);
|
79
79
|
// Send all the non-ai files to the stream
|
80
|
-
this.emitFiles(block.uri, allFiles);
|
80
|
+
this.emitFiles(block.uri, block.aiName, allFiles);
|
81
81
|
const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
|
82
82
|
const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
|
83
83
|
if (uiTemplates.length > 0) {
|
@@ -89,7 +89,7 @@ class StormCodegen {
|
|
89
89
|
prompt: this.userPrompt,
|
90
90
|
});
|
91
91
|
uiStream.on('data', (evt) => {
|
92
|
-
this.handleUiOutput(block.uri, evt);
|
92
|
+
this.handleUiOutput(block.uri, block.aiName, evt);
|
93
93
|
});
|
94
94
|
await uiStream.waitForDone();
|
95
95
|
}
|
@@ -98,13 +98,13 @@ class StormCodegen {
|
|
98
98
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
99
99
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
100
100
|
if (serviceFiles.length > 0) {
|
101
|
-
await this.processTemplates(block.uri, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
101
|
+
await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
102
102
|
}
|
103
103
|
}
|
104
104
|
/**
|
105
105
|
* Emits the text-based files to the stream
|
106
106
|
*/
|
107
|
-
emitFiles(uri, files) {
|
107
|
+
emitFiles(uri, aiName, files) {
|
108
108
|
files.forEach((file) => {
|
109
109
|
if (!file.content || typeof file.content !== 'string') {
|
110
110
|
return;
|
@@ -119,10 +119,10 @@ class StormCodegen {
|
|
119
119
|
// They will need to be implemented by the AI
|
120
120
|
return;
|
121
121
|
}
|
122
|
-
this.emitFile(uri, file.filename, file.content);
|
122
|
+
this.emitFile(uri, aiName, file.filename, file.content);
|
123
123
|
});
|
124
124
|
}
|
125
|
-
emitFile(uri, filename, content, reason = 'File generated') {
|
125
|
+
emitFile(uri, blockName, filename, content, reason = 'File generated') {
|
126
126
|
this.out.emit('data', {
|
127
127
|
type: 'FILE',
|
128
128
|
reason,
|
@@ -130,6 +130,7 @@ class StormCodegen {
|
|
130
130
|
payload: {
|
131
131
|
filename: filename,
|
132
132
|
content: content,
|
133
|
+
blockName,
|
133
134
|
blockRef: uri.toNormalizedString(),
|
134
135
|
},
|
135
136
|
});
|
@@ -137,7 +138,7 @@ class StormCodegen {
|
|
137
138
|
/**
|
138
139
|
* Sends the template to the AI and processes the response
|
139
140
|
*/
|
140
|
-
async processTemplates(blockUri, generator, templates, contextFiles) {
|
141
|
+
async processTemplates(blockUri, aiName, generator, templates, contextFiles) {
|
141
142
|
const promises = templates.map(async (templateFile) => {
|
142
143
|
const stream = await generator({
|
143
144
|
context: contextFiles,
|
@@ -146,7 +147,7 @@ class StormCodegen {
|
|
146
147
|
});
|
147
148
|
const files = [];
|
148
149
|
stream.on('data', (evt) => {
|
149
|
-
const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
|
150
|
+
const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
|
150
151
|
if (file) {
|
151
152
|
files.push(file);
|
152
153
|
}
|
@@ -55,7 +55,6 @@ export declare class StormEventParser {
|
|
55
55
|
getEvents(): StormEvent[];
|
56
56
|
isValid(): boolean;
|
57
57
|
getError(): string;
|
58
|
-
private applyLayoutToBlocks;
|
59
58
|
toResult(handle: string): StormDefinitions;
|
60
59
|
private toSafeName;
|
61
60
|
private toRef;
|
@@ -64,6 +63,7 @@ export declare class StormEventParser {
|
|
64
63
|
};
|
65
64
|
private toResourceKind;
|
66
65
|
private toBlockKind;
|
66
|
+
private toConnectionMapping;
|
67
67
|
private toPortType;
|
68
68
|
private toBlockTarget;
|
69
69
|
private toBlockTargetKind;
|
@@ -3,17 +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
|
-
};
|
9
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
7
|
exports.StormEventParser = exports.resolveOptions = void 0;
|
11
8
|
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
12
9
|
const kaplang_core_1 = require("@kapeta/kaplang-core");
|
13
|
-
const
|
10
|
+
const uuid_1 = require("uuid");
|
14
11
|
const definitionsManager_1 = require("../definitionsManager");
|
15
|
-
const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
|
16
|
-
const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
|
17
12
|
async function resolveOptions() {
|
18
13
|
// Predefined types for now - TODO: Allow user to select / change
|
19
14
|
const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
|
@@ -89,7 +84,6 @@ async function resolveOptions() {
|
|
89
84
|
};
|
90
85
|
}
|
91
86
|
exports.resolveOptions = resolveOptions;
|
92
|
-
const LAYOUT_MARGIN = 50;
|
93
87
|
class StormEventParser {
|
94
88
|
events = [];
|
95
89
|
planName = '';
|
@@ -110,6 +104,7 @@ class StormEventParser {
|
|
110
104
|
}
|
111
105
|
addEvent(handle, evt) {
|
112
106
|
this.events.push(evt);
|
107
|
+
console.log('evt', evt);
|
113
108
|
switch (evt.type) {
|
114
109
|
case 'CREATE_PLAN_PROPERTIES':
|
115
110
|
this.planName = evt.payload.name;
|
@@ -153,55 +148,21 @@ class StormEventParser {
|
|
153
148
|
return this.events;
|
154
149
|
}
|
155
150
|
isValid() {
|
151
|
+
if (!this.planName) {
|
152
|
+
return false;
|
153
|
+
}
|
156
154
|
return !this.failed;
|
157
155
|
}
|
158
156
|
getError() {
|
159
157
|
return this.error;
|
160
158
|
}
|
161
|
-
applyLayoutToBlocks(result) {
|
162
|
-
const graph = (0, ngraph_graph_1.default)();
|
163
|
-
const blockInstances = {};
|
164
|
-
result.plan.spec.blocks.forEach((block, index) => {
|
165
|
-
graph.addNode(block.id, block);
|
166
|
-
blockInstances[block.id] = block;
|
167
|
-
});
|
168
|
-
result.plan.spec.connections.forEach((connection) => {
|
169
|
-
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
170
|
-
});
|
171
|
-
const layout = (0, ngraph_forcelayout_1.default)(graph, {
|
172
|
-
springLength: 150,
|
173
|
-
debug: true,
|
174
|
-
dimensions: 2,
|
175
|
-
gravity: 2,
|
176
|
-
springCoefficient: 0.0008,
|
177
|
-
});
|
178
|
-
for (let i = 0; i < 100; ++i) {
|
179
|
-
layout.step();
|
180
|
-
}
|
181
|
-
// Layout might place things in negative space. We move everything to positive space
|
182
|
-
const graphBox = layout.getGraphRect();
|
183
|
-
let yAdjust = 0;
|
184
|
-
let xAdjust = 0;
|
185
|
-
if (graphBox.y1 < 0) {
|
186
|
-
yAdjust = -graphBox.y1;
|
187
|
-
}
|
188
|
-
if (graphBox.x1 < 0) {
|
189
|
-
xAdjust = -graphBox.x1;
|
190
|
-
}
|
191
|
-
graph.forEachNode((node) => {
|
192
|
-
const position = layout.getNodePosition(node.id);
|
193
|
-
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
194
|
-
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
195
|
-
});
|
196
|
-
layout.dispose();
|
197
|
-
return result;
|
198
|
-
}
|
199
159
|
toResult(handle) {
|
200
|
-
const planRef = this.toRef(handle, this.planName);
|
160
|
+
const planRef = this.toRef(handle, this.planName ?? 'undefined');
|
201
161
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
202
162
|
const refIdMap = {};
|
203
163
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
204
|
-
|
164
|
+
// Create a deterministic uuid
|
165
|
+
const id = (0, uuid_1.v5)(ref, uuid_1.v5.URL);
|
205
166
|
refIdMap[ref] = id;
|
206
167
|
return {
|
207
168
|
id,
|
@@ -249,6 +210,28 @@ class StormEventParser {
|
|
249
210
|
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
|
250
211
|
return;
|
251
212
|
}
|
213
|
+
if (apiProviderBlock.content.spec.entities?.source?.value) {
|
214
|
+
if (!clientConsumerBlock.content.spec.entities) {
|
215
|
+
clientConsumerBlock.content.spec.entities = {
|
216
|
+
types: [],
|
217
|
+
source: {
|
218
|
+
type: kaplang_core_1.KAPLANG_ID,
|
219
|
+
version: kaplang_core_1.KAPLANG_VERSION,
|
220
|
+
value: '',
|
221
|
+
},
|
222
|
+
};
|
223
|
+
}
|
224
|
+
const clientTypes = kaplang_core_1.DSLDataTypeParser.parse(clientConsumerBlock.content.spec.entities.source.value);
|
225
|
+
const apiTypes = kaplang_core_1.DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
|
226
|
+
apiTypes.forEach((apiType) => {
|
227
|
+
if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
|
228
|
+
// Already exists
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
clientTypes.push(apiType);
|
232
|
+
});
|
233
|
+
clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write(clientTypes);
|
234
|
+
}
|
252
235
|
clientResource.spec.methods = apiResource.spec.methods;
|
253
236
|
clientResource.spec.source = apiResource.spec.source;
|
254
237
|
});
|
@@ -267,9 +250,7 @@ class StormEventParser {
|
|
267
250
|
blockId: refIdMap[fromRef.toNormalizedString()],
|
268
251
|
resourceName: connection.fromResource,
|
269
252
|
},
|
270
|
-
mapping:
|
271
|
-
//TODO: Add mapping
|
272
|
-
},
|
253
|
+
mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
|
273
254
|
};
|
274
255
|
});
|
275
256
|
const plan = {
|
@@ -284,10 +265,10 @@ class StormEventParser {
|
|
284
265
|
connections,
|
285
266
|
},
|
286
267
|
};
|
287
|
-
return
|
268
|
+
return {
|
288
269
|
plan,
|
289
270
|
blocks: Object.values(blockDefinitions),
|
290
|
-
}
|
271
|
+
};
|
291
272
|
}
|
292
273
|
toSafeName(name) {
|
293
274
|
return name.toLowerCase().replace(/[^0-9a-z-]/gi, '');
|
@@ -498,6 +479,33 @@ class StormEventParser {
|
|
498
479
|
}
|
499
480
|
return '';
|
500
481
|
}
|
482
|
+
toConnectionMapping(handle, connection, blockDefinitions) {
|
483
|
+
if (connection.fromResourceType !== 'API') {
|
484
|
+
return;
|
485
|
+
}
|
486
|
+
const fromRef = this.toRef(handle, connection.fromComponent);
|
487
|
+
const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
|
488
|
+
if (!apiProviderBlock) {
|
489
|
+
console.warn('Provider block not found: %s', connection.fromComponent, connection);
|
490
|
+
return;
|
491
|
+
}
|
492
|
+
const apiResource = apiProviderBlock.content.spec.providers?.find((p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource);
|
493
|
+
if (!apiResource) {
|
494
|
+
console.warn('API resource not found: %s on %s', connection.fromResource, fromRef.toNormalizedString(), connection);
|
495
|
+
return;
|
496
|
+
}
|
497
|
+
const apiMethods = kaplang_core_1.DSLConverters.toSchemaMethods(kaplang_core_1.DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
|
498
|
+
ignoreSemantics: true,
|
499
|
+
}));
|
500
|
+
const mapping = {};
|
501
|
+
Object.entries(apiMethods).forEach(([methodId, method]) => {
|
502
|
+
mapping[methodId] = {
|
503
|
+
targetId: methodId,
|
504
|
+
type: 'EXACT',
|
505
|
+
};
|
506
|
+
});
|
507
|
+
return mapping;
|
508
|
+
}
|
501
509
|
toPortType(type) {
|
502
510
|
switch (type) {
|
503
511
|
case 'API':
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { StormDefinitions } from
|
5
|
+
import { StormDefinitions } from './event-parser';
|
6
6
|
export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
|
7
7
|
export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
|
8
8
|
export interface StormBlockInfo {
|
@@ -117,6 +117,7 @@ export interface StormEventFile {
|
|
117
117
|
payload: {
|
118
118
|
filename: string;
|
119
119
|
content: string;
|
120
|
+
blockName: string;
|
120
121
|
blockRef: string;
|
121
122
|
};
|
122
123
|
}
|
@@ -28,6 +28,9 @@ class StormClient {
|
|
28
28
|
if (api.hasToken()) {
|
29
29
|
//headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
|
30
30
|
}
|
31
|
+
if (body.conversationId) {
|
32
|
+
headers['conversationId'] = body.conversationId;
|
33
|
+
}
|
31
34
|
return {
|
32
35
|
url,
|
33
36
|
method: method,
|
@@ -150,16 +150,8 @@ describe('event-parser', () => {
|
|
150
150
|
expect(dbResource?.spec.source.value).toContain('type Entity');
|
151
151
|
const serviceBlockInstance = result.plan.spec.blocks[0];
|
152
152
|
expect(serviceBlockInstance.name).toBe('service');
|
153
|
-
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
154
|
-
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
155
|
-
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
156
|
-
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
157
153
|
const uiBlockInstance = result.plan.spec.blocks[1];
|
158
154
|
expect(uiBlockInstance.name).toBe('ui');
|
159
|
-
expect(uiBlockInstance.dimensions.width).toBe(150);
|
160
|
-
expect(uiBlockInstance.dimensions.height).toBe(200);
|
161
|
-
expect(uiBlockInstance.dimensions.top).toBe(107);
|
162
|
-
expect(uiBlockInstance.dimensions.left).toBe(112);
|
163
155
|
expect(result.plan.spec.connections.length).toBe(1);
|
164
156
|
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|
165
157
|
expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
|
@@ -28,15 +28,15 @@ class StormCodegen {
|
|
28
28
|
getStream() {
|
29
29
|
return this.out;
|
30
30
|
}
|
31
|
-
handleTemplateFileOutput(blockUri, template, data) {
|
31
|
+
handleTemplateFileOutput(blockUri, aiName, template, data) {
|
32
32
|
switch (data.type) {
|
33
33
|
case 'FILE':
|
34
34
|
template.filename = data.payload.filename;
|
35
35
|
template.content = data.payload.content;
|
36
|
-
return this.handleFileOutput(blockUri, data);
|
36
|
+
return this.handleFileOutput(blockUri, aiName, data);
|
37
37
|
}
|
38
38
|
}
|
39
|
-
handleUiOutput(blockUri, data) {
|
39
|
+
handleUiOutput(blockUri, aiName, data) {
|
40
40
|
switch (data.type) {
|
41
41
|
case 'SCREEN':
|
42
42
|
this.out.emit('data', {
|
@@ -45,17 +45,17 @@ class StormCodegen {
|
|
45
45
|
created: Date.now(),
|
46
46
|
payload: {
|
47
47
|
...data.payload,
|
48
|
-
blockName:
|
48
|
+
blockName: aiName,
|
49
49
|
},
|
50
50
|
});
|
51
51
|
case 'FILE':
|
52
|
-
return this.handleFileOutput(blockUri, data);
|
52
|
+
return this.handleFileOutput(blockUri, aiName, data);
|
53
53
|
}
|
54
54
|
}
|
55
|
-
handleFileOutput(blockUri, data) {
|
55
|
+
handleFileOutput(blockUri, aiName, data) {
|
56
56
|
switch (data.type) {
|
57
57
|
case 'FILE':
|
58
|
-
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
58
|
+
this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
|
59
59
|
return {
|
60
60
|
type: 'FILE',
|
61
61
|
created: Date.now(),
|
@@ -77,7 +77,7 @@ class StormCodegen {
|
|
77
77
|
}
|
78
78
|
const allFiles = this.toStormFiles(generatedResult);
|
79
79
|
// Send all the non-ai files to the stream
|
80
|
-
this.emitFiles(block.uri, allFiles);
|
80
|
+
this.emitFiles(block.uri, block.aiName, allFiles);
|
81
81
|
const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
|
82
82
|
const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
|
83
83
|
if (uiTemplates.length > 0) {
|
@@ -89,7 +89,7 @@ class StormCodegen {
|
|
89
89
|
prompt: this.userPrompt,
|
90
90
|
});
|
91
91
|
uiStream.on('data', (evt) => {
|
92
|
-
this.handleUiOutput(block.uri, evt);
|
92
|
+
this.handleUiOutput(block.uri, block.aiName, evt);
|
93
93
|
});
|
94
94
|
await uiStream.waitForDone();
|
95
95
|
}
|
@@ -98,13 +98,13 @@ class StormCodegen {
|
|
98
98
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
99
99
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
100
100
|
if (serviceFiles.length > 0) {
|
101
|
-
await this.processTemplates(block.uri, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
101
|
+
await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
102
102
|
}
|
103
103
|
}
|
104
104
|
/**
|
105
105
|
* Emits the text-based files to the stream
|
106
106
|
*/
|
107
|
-
emitFiles(uri, files) {
|
107
|
+
emitFiles(uri, aiName, files) {
|
108
108
|
files.forEach((file) => {
|
109
109
|
if (!file.content || typeof file.content !== 'string') {
|
110
110
|
return;
|
@@ -119,10 +119,10 @@ class StormCodegen {
|
|
119
119
|
// They will need to be implemented by the AI
|
120
120
|
return;
|
121
121
|
}
|
122
|
-
this.emitFile(uri, file.filename, file.content);
|
122
|
+
this.emitFile(uri, aiName, file.filename, file.content);
|
123
123
|
});
|
124
124
|
}
|
125
|
-
emitFile(uri, filename, content, reason = 'File generated') {
|
125
|
+
emitFile(uri, blockName, filename, content, reason = 'File generated') {
|
126
126
|
this.out.emit('data', {
|
127
127
|
type: 'FILE',
|
128
128
|
reason,
|
@@ -130,6 +130,7 @@ class StormCodegen {
|
|
130
130
|
payload: {
|
131
131
|
filename: filename,
|
132
132
|
content: content,
|
133
|
+
blockName,
|
133
134
|
blockRef: uri.toNormalizedString(),
|
134
135
|
},
|
135
136
|
});
|
@@ -137,7 +138,7 @@ class StormCodegen {
|
|
137
138
|
/**
|
138
139
|
* Sends the template to the AI and processes the response
|
139
140
|
*/
|
140
|
-
async processTemplates(blockUri, generator, templates, contextFiles) {
|
141
|
+
async processTemplates(blockUri, aiName, generator, templates, contextFiles) {
|
141
142
|
const promises = templates.map(async (templateFile) => {
|
142
143
|
const stream = await generator({
|
143
144
|
context: contextFiles,
|
@@ -146,7 +147,7 @@ class StormCodegen {
|
|
146
147
|
});
|
147
148
|
const files = [];
|
148
149
|
stream.on('data', (evt) => {
|
149
|
-
const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
|
150
|
+
const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
|
150
151
|
if (file) {
|
151
152
|
files.push(file);
|
152
153
|
}
|
@@ -55,7 +55,6 @@ export declare class StormEventParser {
|
|
55
55
|
getEvents(): StormEvent[];
|
56
56
|
isValid(): boolean;
|
57
57
|
getError(): string;
|
58
|
-
private applyLayoutToBlocks;
|
59
58
|
toResult(handle: string): StormDefinitions;
|
60
59
|
private toSafeName;
|
61
60
|
private toRef;
|
@@ -64,6 +63,7 @@ export declare class StormEventParser {
|
|
64
63
|
};
|
65
64
|
private toResourceKind;
|
66
65
|
private toBlockKind;
|
66
|
+
private toConnectionMapping;
|
67
67
|
private toPortType;
|
68
68
|
private toBlockTarget;
|
69
69
|
private toBlockTargetKind;
|
@@ -3,17 +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
|
-
};
|
9
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
7
|
exports.StormEventParser = exports.resolveOptions = void 0;
|
11
8
|
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
12
9
|
const kaplang_core_1 = require("@kapeta/kaplang-core");
|
13
|
-
const
|
10
|
+
const uuid_1 = require("uuid");
|
14
11
|
const definitionsManager_1 = require("../definitionsManager");
|
15
|
-
const ngraph_graph_1 = __importDefault(require("ngraph.graph"));
|
16
|
-
const ngraph_forcelayout_1 = __importDefault(require("ngraph.forcelayout"));
|
17
12
|
async function resolveOptions() {
|
18
13
|
// Predefined types for now - TODO: Allow user to select / change
|
19
14
|
const blockTypeService = await definitionsManager_1.definitionsManager.getLatestDefinition('kapeta/block-type-service');
|
@@ -89,7 +84,6 @@ async function resolveOptions() {
|
|
89
84
|
};
|
90
85
|
}
|
91
86
|
exports.resolveOptions = resolveOptions;
|
92
|
-
const LAYOUT_MARGIN = 50;
|
93
87
|
class StormEventParser {
|
94
88
|
events = [];
|
95
89
|
planName = '';
|
@@ -110,6 +104,7 @@ class StormEventParser {
|
|
110
104
|
}
|
111
105
|
addEvent(handle, evt) {
|
112
106
|
this.events.push(evt);
|
107
|
+
console.log('evt', evt);
|
113
108
|
switch (evt.type) {
|
114
109
|
case 'CREATE_PLAN_PROPERTIES':
|
115
110
|
this.planName = evt.payload.name;
|
@@ -153,55 +148,21 @@ class StormEventParser {
|
|
153
148
|
return this.events;
|
154
149
|
}
|
155
150
|
isValid() {
|
151
|
+
if (!this.planName) {
|
152
|
+
return false;
|
153
|
+
}
|
156
154
|
return !this.failed;
|
157
155
|
}
|
158
156
|
getError() {
|
159
157
|
return this.error;
|
160
158
|
}
|
161
|
-
applyLayoutToBlocks(result) {
|
162
|
-
const graph = (0, ngraph_graph_1.default)();
|
163
|
-
const blockInstances = {};
|
164
|
-
result.plan.spec.blocks.forEach((block, index) => {
|
165
|
-
graph.addNode(block.id, block);
|
166
|
-
blockInstances[block.id] = block;
|
167
|
-
});
|
168
|
-
result.plan.spec.connections.forEach((connection) => {
|
169
|
-
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
170
|
-
});
|
171
|
-
const layout = (0, ngraph_forcelayout_1.default)(graph, {
|
172
|
-
springLength: 150,
|
173
|
-
debug: true,
|
174
|
-
dimensions: 2,
|
175
|
-
gravity: 2,
|
176
|
-
springCoefficient: 0.0008,
|
177
|
-
});
|
178
|
-
for (let i = 0; i < 100; ++i) {
|
179
|
-
layout.step();
|
180
|
-
}
|
181
|
-
// Layout might place things in negative space. We move everything to positive space
|
182
|
-
const graphBox = layout.getGraphRect();
|
183
|
-
let yAdjust = 0;
|
184
|
-
let xAdjust = 0;
|
185
|
-
if (graphBox.y1 < 0) {
|
186
|
-
yAdjust = -graphBox.y1;
|
187
|
-
}
|
188
|
-
if (graphBox.x1 < 0) {
|
189
|
-
xAdjust = -graphBox.x1;
|
190
|
-
}
|
191
|
-
graph.forEachNode((node) => {
|
192
|
-
const position = layout.getNodePosition(node.id);
|
193
|
-
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
194
|
-
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
195
|
-
});
|
196
|
-
layout.dispose();
|
197
|
-
return result;
|
198
|
-
}
|
199
159
|
toResult(handle) {
|
200
|
-
const planRef = this.toRef(handle, this.planName);
|
160
|
+
const planRef = this.toRef(handle, this.planName ?? 'undefined');
|
201
161
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
202
162
|
const refIdMap = {};
|
203
163
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
204
|
-
|
164
|
+
// Create a deterministic uuid
|
165
|
+
const id = (0, uuid_1.v5)(ref, uuid_1.v5.URL);
|
205
166
|
refIdMap[ref] = id;
|
206
167
|
return {
|
207
168
|
id,
|
@@ -249,6 +210,28 @@ class StormEventParser {
|
|
249
210
|
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
|
250
211
|
return;
|
251
212
|
}
|
213
|
+
if (apiProviderBlock.content.spec.entities?.source?.value) {
|
214
|
+
if (!clientConsumerBlock.content.spec.entities) {
|
215
|
+
clientConsumerBlock.content.spec.entities = {
|
216
|
+
types: [],
|
217
|
+
source: {
|
218
|
+
type: kaplang_core_1.KAPLANG_ID,
|
219
|
+
version: kaplang_core_1.KAPLANG_VERSION,
|
220
|
+
value: '',
|
221
|
+
},
|
222
|
+
};
|
223
|
+
}
|
224
|
+
const clientTypes = kaplang_core_1.DSLDataTypeParser.parse(clientConsumerBlock.content.spec.entities.source.value);
|
225
|
+
const apiTypes = kaplang_core_1.DSLDataTypeParser.parse(apiProviderBlock.content.spec.entities?.source?.value);
|
226
|
+
apiTypes.forEach((apiType) => {
|
227
|
+
if (clientTypes.some((clientType) => clientType.name === apiType.name)) {
|
228
|
+
// Already exists
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
clientTypes.push(apiType);
|
232
|
+
});
|
233
|
+
clientConsumerBlock.content.spec.entities.source.value = kaplang_core_1.KaplangWriter.write(clientTypes);
|
234
|
+
}
|
252
235
|
clientResource.spec.methods = apiResource.spec.methods;
|
253
236
|
clientResource.spec.source = apiResource.spec.source;
|
254
237
|
});
|
@@ -267,9 +250,7 @@ class StormEventParser {
|
|
267
250
|
blockId: refIdMap[fromRef.toNormalizedString()],
|
268
251
|
resourceName: connection.fromResource,
|
269
252
|
},
|
270
|
-
mapping:
|
271
|
-
//TODO: Add mapping
|
272
|
-
},
|
253
|
+
mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
|
273
254
|
};
|
274
255
|
});
|
275
256
|
const plan = {
|
@@ -284,10 +265,10 @@ class StormEventParser {
|
|
284
265
|
connections,
|
285
266
|
},
|
286
267
|
};
|
287
|
-
return
|
268
|
+
return {
|
288
269
|
plan,
|
289
270
|
blocks: Object.values(blockDefinitions),
|
290
|
-
}
|
271
|
+
};
|
291
272
|
}
|
292
273
|
toSafeName(name) {
|
293
274
|
return name.toLowerCase().replace(/[^0-9a-z-]/gi, '');
|
@@ -498,6 +479,33 @@ class StormEventParser {
|
|
498
479
|
}
|
499
480
|
return '';
|
500
481
|
}
|
482
|
+
toConnectionMapping(handle, connection, blockDefinitions) {
|
483
|
+
if (connection.fromResourceType !== 'API') {
|
484
|
+
return;
|
485
|
+
}
|
486
|
+
const fromRef = this.toRef(handle, connection.fromComponent);
|
487
|
+
const apiProviderBlock = blockDefinitions[fromRef.toNormalizedString()];
|
488
|
+
if (!apiProviderBlock) {
|
489
|
+
console.warn('Provider block not found: %s', connection.fromComponent, connection);
|
490
|
+
return;
|
491
|
+
}
|
492
|
+
const apiResource = apiProviderBlock.content.spec.providers?.find((p) => p.kind === this.options.apiKind && p.metadata.name === connection.fromResource);
|
493
|
+
if (!apiResource) {
|
494
|
+
console.warn('API resource not found: %s on %s', connection.fromResource, fromRef.toNormalizedString(), connection);
|
495
|
+
return;
|
496
|
+
}
|
497
|
+
const apiMethods = kaplang_core_1.DSLConverters.toSchemaMethods(kaplang_core_1.DSLAPIParser.parse(apiResource.spec?.source?.value ?? '', {
|
498
|
+
ignoreSemantics: true,
|
499
|
+
}));
|
500
|
+
const mapping = {};
|
501
|
+
Object.entries(apiMethods).forEach(([methodId, method]) => {
|
502
|
+
mapping[methodId] = {
|
503
|
+
targetId: methodId,
|
504
|
+
type: 'EXACT',
|
505
|
+
};
|
506
|
+
});
|
507
|
+
return mapping;
|
508
|
+
}
|
501
509
|
toPortType(type) {
|
502
510
|
switch (type) {
|
503
511
|
case 'API':
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { StormDefinitions } from
|
5
|
+
import { StormDefinitions } from './event-parser';
|
6
6
|
export type StormResourceType = 'API' | 'DATABASE' | 'CLIENT' | 'JWTPROVIDER' | 'JWTCONSUMER' | 'WEBFRAGMENT' | 'WEBPAGE' | 'SMTPCLIENT' | 'EXTERNAL_API' | 'SUBSCRIBER' | 'PUBLISHER' | 'QUEUE' | 'EXCHANGE';
|
7
7
|
export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
|
8
8
|
export interface StormBlockInfo {
|
@@ -117,6 +117,7 @@ export interface StormEventFile {
|
|
117
117
|
payload: {
|
118
118
|
filename: string;
|
119
119
|
content: string;
|
120
|
+
blockName: string;
|
120
121
|
blockRef: string;
|
121
122
|
};
|
122
123
|
}
|
@@ -28,6 +28,9 @@ class StormClient {
|
|
28
28
|
if (api.hasToken()) {
|
29
29
|
//headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
|
30
30
|
}
|
31
|
+
if (body.conversationId) {
|
32
|
+
headers['conversationId'] = body.conversationId;
|
33
|
+
}
|
31
34
|
return {
|
32
35
|
url,
|
33
36
|
method: method,
|
@@ -150,16 +150,8 @@ describe('event-parser', () => {
|
|
150
150
|
expect(dbResource?.spec.source.value).toContain('type Entity');
|
151
151
|
const serviceBlockInstance = result.plan.spec.blocks[0];
|
152
152
|
expect(serviceBlockInstance.name).toBe('service');
|
153
|
-
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
154
|
-
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
155
|
-
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
156
|
-
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
157
153
|
const uiBlockInstance = result.plan.spec.blocks[1];
|
158
154
|
expect(uiBlockInstance.name).toBe('ui');
|
159
|
-
expect(uiBlockInstance.dimensions.width).toBe(150);
|
160
|
-
expect(uiBlockInstance.dimensions.height).toBe(200);
|
161
|
-
expect(uiBlockInstance.dimensions.top).toBe(107);
|
162
|
-
expect(uiBlockInstance.dimensions.left).toBe(112);
|
163
155
|
expect(result.plan.spec.connections.length).toBe(1);
|
164
156
|
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|
165
157
|
expect(result.plan.spec.connections[0].consumer.resourceName).toBe(clientResource?.metadata.name);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.47.
|
3
|
+
"version": "0.47.1",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -74,8 +74,6 @@
|
|
74
74
|
"gunzip-maybe": "^1.4.2",
|
75
75
|
"lodash": "^4.17.15",
|
76
76
|
"md5": "2.2.1",
|
77
|
-
"ngraph.forcelayout": "^3.3.1",
|
78
|
-
"ngraph.graph": "^20.0.1",
|
79
77
|
"node-cache": "^5.1.2",
|
80
78
|
"node-uuid": "^1.4.8",
|
81
79
|
"parse-data-uri": "^0.2.0",
|
@@ -85,6 +83,7 @@
|
|
85
83
|
"stream-json": "^1.8.0",
|
86
84
|
"tar-stream": "^3.1.6",
|
87
85
|
"typescript": "^5.1.6",
|
86
|
+
"uuid": "^9.0.1",
|
88
87
|
"yaml": "^1.6.0"
|
89
88
|
},
|
90
89
|
"devDependencies": {
|
@@ -103,6 +102,7 @@
|
|
103
102
|
"@types/node-uuid": "^0.0.29",
|
104
103
|
"@types/request": "^2.48.8",
|
105
104
|
"@types/tar-stream": "^2.2.2",
|
105
|
+
"@types/uuid": "^9.0.8",
|
106
106
|
"eslint": "^8.42.0",
|
107
107
|
"eslint-config-prettier": "^8.8.0",
|
108
108
|
"jest": "^29.6.4",
|
package/src/storm/codegen.ts
CHANGED
@@ -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 {
|
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:
|
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(
|
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
|
}
|
@@ -14,11 +14,18 @@ import {
|
|
14
14
|
SourceCode,
|
15
15
|
} from '@kapeta/schemas';
|
16
16
|
import { KapetaURI, normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
|
17
|
-
import {
|
18
|
-
|
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';
|
19
28
|
import { definitionsManager } from '../definitionsManager';
|
20
|
-
import createGraph from 'ngraph.graph';
|
21
|
-
import createLayout from 'ngraph.forcelayout';
|
22
29
|
|
23
30
|
export interface BlockDefinitionInfo {
|
24
31
|
uri: KapetaURI;
|
@@ -170,8 +177,6 @@ export async function resolveOptions(): Promise<StormOptions> {
|
|
170
177
|
};
|
171
178
|
}
|
172
179
|
|
173
|
-
const LAYOUT_MARGIN = 50;
|
174
|
-
|
175
180
|
export class StormEventParser {
|
176
181
|
private events: StormEvent[] = [];
|
177
182
|
private planName: string = '';
|
@@ -193,8 +198,9 @@ export class StormEventParser {
|
|
193
198
|
this.connections = [];
|
194
199
|
}
|
195
200
|
|
196
|
-
public addEvent(handle:string, evt: StormEvent): StormDefinitions {
|
201
|
+
public addEvent(handle: string, evt: StormEvent): StormDefinitions {
|
197
202
|
this.events.push(evt);
|
203
|
+
console.log('evt', evt);
|
198
204
|
switch (evt.type) {
|
199
205
|
case 'CREATE_PLAN_PROPERTIES':
|
200
206
|
this.planName = evt.payload.name;
|
@@ -242,6 +248,9 @@ export class StormEventParser {
|
|
242
248
|
}
|
243
249
|
|
244
250
|
public isValid(): boolean {
|
251
|
+
if (!this.planName) {
|
252
|
+
return false;
|
253
|
+
}
|
245
254
|
return !this.failed;
|
246
255
|
}
|
247
256
|
|
@@ -249,59 +258,13 @@ export class StormEventParser {
|
|
249
258
|
return this.error;
|
250
259
|
}
|
251
260
|
|
252
|
-
private applyLayoutToBlocks(result: StormDefinitions): StormDefinitions {
|
253
|
-
const graph = createGraph();
|
254
|
-
const blockInstances: { [key: string]: BlockInstance } = {};
|
255
|
-
|
256
|
-
result.plan.spec.blocks.forEach((block, index) => {
|
257
|
-
graph.addNode(block.id, block);
|
258
|
-
blockInstances[block.id] = block;
|
259
|
-
});
|
260
|
-
|
261
|
-
result.plan.spec.connections.forEach((connection) => {
|
262
|
-
graph.addLink(connection.provider.blockId, connection.consumer.blockId);
|
263
|
-
});
|
264
|
-
|
265
|
-
const layout = createLayout(graph, {
|
266
|
-
springLength: 150,
|
267
|
-
debug: true,
|
268
|
-
dimensions: 2,
|
269
|
-
gravity: 2,
|
270
|
-
springCoefficient: 0.0008,
|
271
|
-
});
|
272
|
-
|
273
|
-
for (let i = 0; i < 100; ++i) {
|
274
|
-
layout.step();
|
275
|
-
}
|
276
|
-
|
277
|
-
// Layout might place things in negative space. We move everything to positive space
|
278
|
-
const graphBox = layout.getGraphRect();
|
279
|
-
let yAdjust = 0;
|
280
|
-
let xAdjust = 0;
|
281
|
-
if (graphBox.y1 < 0) {
|
282
|
-
yAdjust = -graphBox.y1;
|
283
|
-
}
|
284
|
-
if (graphBox.x1 < 0) {
|
285
|
-
xAdjust = -graphBox.x1;
|
286
|
-
}
|
287
|
-
|
288
|
-
graph.forEachNode((node) => {
|
289
|
-
const position = layout.getNodePosition(node.id);
|
290
|
-
blockInstances[node.id].dimensions.left = LAYOUT_MARGIN + Math.round(position.x + xAdjust);
|
291
|
-
blockInstances[node.id].dimensions.top = LAYOUT_MARGIN + Math.round(position.y + yAdjust);
|
292
|
-
});
|
293
|
-
|
294
|
-
layout.dispose();
|
295
|
-
|
296
|
-
return result;
|
297
|
-
}
|
298
|
-
|
299
261
|
public toResult(handle: string): StormDefinitions {
|
300
|
-
const planRef = this.toRef(handle, this.planName);
|
262
|
+
const planRef = this.toRef(handle, this.planName ?? 'undefined');
|
301
263
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
302
264
|
const refIdMap: { [key: string]: string } = {};
|
303
265
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
304
|
-
|
266
|
+
// Create a deterministic uuid
|
267
|
+
const id = uuid(ref, uuid.URL);
|
305
268
|
refIdMap[ref] = id;
|
306
269
|
return {
|
307
270
|
id,
|
@@ -368,13 +331,41 @@ export class StormEventParser {
|
|
368
331
|
return;
|
369
332
|
}
|
370
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
|
+
}
|
371
361
|
clientResource.spec.methods = apiResource.spec.methods;
|
372
362
|
clientResource.spec.source = apiResource.spec.source;
|
373
363
|
});
|
374
364
|
|
375
|
-
const connections = this.connections.map((connection) => {
|
365
|
+
const connections: Connection[] = this.connections.map((connection) => {
|
376
366
|
const fromRef = this.toRef(handle, connection.fromComponent);
|
377
367
|
const toRef = this.toRef(handle, connection.toComponent);
|
368
|
+
|
378
369
|
return {
|
379
370
|
port: {
|
380
371
|
type: this.toPortType(connection.fromResourceType),
|
@@ -387,9 +378,7 @@ export class StormEventParser {
|
|
387
378
|
blockId: refIdMap[fromRef.toNormalizedString()],
|
388
379
|
resourceName: connection.fromResource,
|
389
380
|
},
|
390
|
-
mapping:
|
391
|
-
//TODO: Add mapping
|
392
|
-
},
|
381
|
+
mapping: this.toConnectionMapping(handle, connection, blockDefinitions),
|
393
382
|
} satisfies Connection;
|
394
383
|
});
|
395
384
|
|
@@ -406,10 +395,10 @@ export class StormEventParser {
|
|
406
395
|
},
|
407
396
|
};
|
408
397
|
|
409
|
-
return
|
398
|
+
return {
|
410
399
|
plan,
|
411
400
|
blocks: Object.values(blockDefinitions),
|
412
|
-
}
|
401
|
+
};
|
413
402
|
}
|
414
403
|
|
415
404
|
private toSafeName(name: string): string {
|
@@ -637,6 +626,55 @@ export class StormEventParser {
|
|
637
626
|
return '';
|
638
627
|
}
|
639
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
|
+
|
640
678
|
private toPortType(type: StormResourceType) {
|
641
679
|
switch (type) {
|
642
680
|
case 'API':
|
package/src/storm/events.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import {StormDefinitions} from
|
5
|
+
import { StormDefinitions } from './event-parser';
|
6
6
|
|
7
7
|
export type StormResourceType =
|
8
8
|
| 'API'
|
@@ -146,6 +146,7 @@ export interface StormEventFile {
|
|
146
146
|
payload: {
|
147
147
|
filename: string;
|
148
148
|
content: string;
|
149
|
+
blockName: string;
|
149
150
|
blockRef: string;
|
150
151
|
};
|
151
152
|
}
|
package/src/storm/routes.ts
CHANGED
@@ -4,15 +4,15 @@
|
|
4
4
|
*/
|
5
5
|
|
6
6
|
import Router from 'express-promise-router';
|
7
|
-
import {Response} from 'express';
|
8
|
-
import {corsHandler} from '../middleware/cors';
|
9
|
-
import {stringBody} from '../middleware/stringBody';
|
10
|
-
import {KapetaBodyRequest} from '../types';
|
11
|
-
import {StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream} from './stream';
|
12
|
-
import {stormClient} from './stormClient';
|
13
|
-
import {StormEvent} from './events';
|
14
|
-
import {resolveOptions, StormDefinitions, StormEventParser} from './event-parser';
|
15
|
-
import {StormCodegen} from './codegen';
|
7
|
+
import { Response } from 'express';
|
8
|
+
import { corsHandler } from '../middleware/cors';
|
9
|
+
import { stringBody } from '../middleware/stringBody';
|
10
|
+
import { KapetaBodyRequest } from '../types';
|
11
|
+
import { StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
|
12
|
+
import { stormClient } from './stormClient';
|
13
|
+
import { StormEvent } from './events';
|
14
|
+
import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
|
15
|
+
import { StormCodegen } from './codegen';
|
16
16
|
|
17
17
|
const router = Router();
|
18
18
|
|
@@ -44,7 +44,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
44
44
|
// We can't continue if the meta stream is invalid
|
45
45
|
sendEvent(res, {
|
46
46
|
type: 'ERROR_INTERNAL',
|
47
|
-
payload: {error: eventParser.getError()},
|
47
|
+
payload: { error: eventParser.getError() },
|
48
48
|
reason: 'Failed to generate system',
|
49
49
|
created: Date.now(),
|
50
50
|
});
|
@@ -94,15 +94,14 @@ function sendError(err: Error, res: Response) {
|
|
94
94
|
sendEvent(res, {
|
95
95
|
type: 'ERROR_INTERNAL',
|
96
96
|
created: Date.now(),
|
97
|
-
payload: {error: err.message},
|
97
|
+
payload: { error: err.message },
|
98
98
|
reason: 'Failed while sending prompt',
|
99
99
|
});
|
100
100
|
} else {
|
101
|
-
res.status(400).send({error: err.message});
|
101
|
+
res.status(400).send({ error: err.message });
|
102
102
|
}
|
103
103
|
}
|
104
104
|
|
105
|
-
|
106
105
|
function streamStormPartialResponse(result: StormStream, res: Response) {
|
107
106
|
return new Promise<void>((resolve, reject) => {
|
108
107
|
result.on('data', (data) => {
|
package/src/storm/stormClient.ts
CHANGED
package/src/storm/stream.ts
CHANGED
@@ -169,17 +169,9 @@ describe('event-parser', () => {
|
|
169
169
|
|
170
170
|
const serviceBlockInstance = result.plan.spec.blocks[0];
|
171
171
|
expect(serviceBlockInstance.name).toBe('service');
|
172
|
-
expect(serviceBlockInstance.dimensions.width).toBe(150);
|
173
|
-
expect(serviceBlockInstance.dimensions.height).toBe(200);
|
174
|
-
expect(serviceBlockInstance.dimensions.top).toBe(3);
|
175
|
-
expect(serviceBlockInstance.dimensions.left).toBe(6);
|
176
172
|
|
177
173
|
const uiBlockInstance = result.plan.spec.blocks[1];
|
178
174
|
expect(uiBlockInstance.name).toBe('ui');
|
179
|
-
expect(uiBlockInstance.dimensions.width).toBe(150);
|
180
|
-
expect(uiBlockInstance.dimensions.height).toBe(200);
|
181
|
-
expect(uiBlockInstance.dimensions.top).toBe(107);
|
182
|
-
expect(uiBlockInstance.dimensions.left).toBe(112);
|
183
175
|
|
184
176
|
expect(result.plan.spec.connections.length).toBe(1);
|
185
177
|
expect(result.plan.spec.connections[0].consumer.blockId).toBe(uiBlockInstance.id);
|