@kapeta/local-cluster-service 0.45.0 → 0.46.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 +7 -0
- package/dist/cjs/src/storm/codegen.d.ts +5 -1
- package/dist/cjs/src/storm/codegen.js +68 -18
- package/dist/cjs/src/storm/event-parser.d.ts +2 -2
- package/dist/cjs/src/storm/event-parser.js +8 -29
- package/dist/cjs/src/storm/events.d.ts +0 -1
- package/dist/cjs/src/storm/routes.js +1 -23
- package/dist/cjs/src/storm/stormClient.d.ts +2 -2
- package/dist/cjs/src/storm/stormClient.js +0 -7
- package/dist/cjs/src/storm/stream.d.ts +7 -0
- package/dist/esm/src/storm/codegen.d.ts +5 -1
- package/dist/esm/src/storm/codegen.js +68 -18
- package/dist/esm/src/storm/event-parser.d.ts +2 -2
- package/dist/esm/src/storm/event-parser.js +8 -29
- package/dist/esm/src/storm/events.d.ts +0 -1
- package/dist/esm/src/storm/routes.js +1 -23
- package/dist/esm/src/storm/stormClient.d.ts +2 -2
- package/dist/esm/src/storm/stormClient.js +0 -7
- package/dist/esm/src/storm/stream.d.ts +7 -0
- package/package.json +1 -1
- package/src/storm/codegen.ts +83 -27
- package/src/storm/event-parser.ts +12 -32
- package/src/storm/events.ts +0 -1
- package/src/storm/routes.ts +1 -33
- package/src/storm/stormClient.ts +3 -11
- package/src/storm/stream.ts +8 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.46.0](https://github.com/kapetacom/local-cluster-service/compare/v0.45.0...v0.46.0) (2024-05-28)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Change how the UI AI work to get templates from codegen ([#148](https://github.com/kapetacom/local-cluster-service/issues/148)) ([f757ec8](https://github.com/kapetacom/local-cluster-service/commit/f757ec8e4fab0dbb58b96a1b9e25f3e734ddb10f))
|
7
|
+
|
1
8
|
# [0.45.0](https://github.com/kapetacom/local-cluster-service/compare/v0.44.0...v0.45.0) (2024-05-24)
|
2
9
|
|
3
10
|
|
@@ -2,15 +2,19 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
+
import { StormEvent } from './events';
|
5
6
|
import { BlockDefinitionInfo } from './event-parser';
|
6
7
|
import { StormStream } from './stream';
|
7
8
|
export declare class StormCodegen {
|
8
9
|
private readonly userPrompt;
|
9
10
|
private readonly blocks;
|
10
11
|
private readonly out;
|
11
|
-
|
12
|
+
private readonly events;
|
13
|
+
constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
|
12
14
|
process(): Promise<void>;
|
13
15
|
getStream(): StormStream;
|
16
|
+
private handleTemplateFileOutput;
|
17
|
+
private handleUiOutput;
|
14
18
|
private handleFileOutput;
|
15
19
|
/**
|
16
20
|
* Generates the code for a block and sends it to the AI
|
@@ -13,12 +13,13 @@ class StormCodegen {
|
|
13
13
|
userPrompt;
|
14
14
|
blocks;
|
15
15
|
out = new stream_1.StormStream();
|
16
|
-
|
16
|
+
events;
|
17
|
+
constructor(userPrompt, blocks, events) {
|
17
18
|
this.userPrompt = userPrompt;
|
18
19
|
this.blocks = blocks;
|
20
|
+
this.events = events;
|
19
21
|
}
|
20
22
|
async process() {
|
21
|
-
console.log('Processing blocks', this.blocks.length);
|
22
23
|
for (const block of this.blocks) {
|
23
24
|
await this.processBlockCode(block);
|
24
25
|
}
|
@@ -27,13 +28,42 @@ class StormCodegen {
|
|
27
28
|
getStream() {
|
28
29
|
return this.out;
|
29
30
|
}
|
30
|
-
|
31
|
+
handleTemplateFileOutput(blockUri, template, data) {
|
31
32
|
switch (data.type) {
|
32
33
|
case 'FILE':
|
33
34
|
template.filename = data.payload.filename;
|
34
35
|
template.content = data.payload.content;
|
36
|
+
return this.handleFileOutput(blockUri, data);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
handleUiOutput(blockUri, data) {
|
40
|
+
switch (data.type) {
|
41
|
+
case 'SCREEN':
|
42
|
+
this.out.emit('data', {
|
43
|
+
type: 'SCREEN',
|
44
|
+
reason: data.reason,
|
45
|
+
created: Date.now(),
|
46
|
+
payload: {
|
47
|
+
...data.payload,
|
48
|
+
blockName: blockUri.toNormalizedString(),
|
49
|
+
},
|
50
|
+
});
|
51
|
+
case 'FILE':
|
52
|
+
return this.handleFileOutput(blockUri, data);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
handleFileOutput(blockUri, data) {
|
56
|
+
switch (data.type) {
|
57
|
+
case 'FILE':
|
35
58
|
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
36
|
-
|
59
|
+
return {
|
60
|
+
type: 'FILE',
|
61
|
+
created: Date.now(),
|
62
|
+
payload: {
|
63
|
+
filename: data.payload.filename,
|
64
|
+
content: data.payload.content,
|
65
|
+
},
|
66
|
+
};
|
37
67
|
}
|
38
68
|
}
|
39
69
|
/**
|
@@ -41,22 +71,35 @@ class StormCodegen {
|
|
41
71
|
*/
|
42
72
|
async processBlockCode(block) {
|
43
73
|
// Generate the code for the block using the standard codegen templates
|
44
|
-
const generatedResult = await this.generateBlock(block.content
|
74
|
+
const generatedResult = await this.generateBlock(block.content);
|
45
75
|
if (!generatedResult) {
|
46
|
-
console.warn('No generated result for block', block.uri);
|
47
76
|
return;
|
48
77
|
}
|
49
78
|
const allFiles = this.toStormFiles(generatedResult);
|
50
79
|
// Send all the non-ai files to the stream
|
51
80
|
this.emitFiles(block.uri, allFiles);
|
52
|
-
const
|
53
|
-
|
54
|
-
|
81
|
+
const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
|
82
|
+
const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
|
83
|
+
if (uiTemplates.length > 0) {
|
84
|
+
const uiStream = await stormClient_1.stormClient.createUIImplementation({
|
85
|
+
events: this.events,
|
86
|
+
templates: uiTemplates,
|
87
|
+
context: relevantFiles,
|
88
|
+
blockName: block.aiName,
|
89
|
+
prompt: this.userPrompt,
|
90
|
+
});
|
91
|
+
uiStream.on('data', (evt) => {
|
92
|
+
this.handleUiOutput(block.uri, evt);
|
93
|
+
});
|
94
|
+
await uiStream.waitForDone();
|
95
|
+
}
|
96
|
+
// Gather the context files for implementation. These will be all be passed to the AI
|
97
|
+
const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
|
55
98
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
56
99
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
57
|
-
|
58
|
-
|
59
|
-
|
100
|
+
if (serviceFiles.length > 0) {
|
101
|
+
await this.processTemplates(block.uri, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
102
|
+
}
|
60
103
|
}
|
61
104
|
/**
|
62
105
|
* Emits the text-based files to the stream
|
@@ -94,17 +137,25 @@ class StormCodegen {
|
|
94
137
|
/**
|
95
138
|
* Sends the template to the AI and processes the response
|
96
139
|
*/
|
97
|
-
processTemplates(blockUri, generator, templates, contextFiles) {
|
140
|
+
async processTemplates(blockUri, generator, templates, contextFiles) {
|
98
141
|
const promises = templates.map(async (templateFile) => {
|
99
142
|
const stream = await generator({
|
100
143
|
context: contextFiles,
|
101
144
|
template: templateFile,
|
102
145
|
prompt: this.userPrompt,
|
103
146
|
});
|
104
|
-
|
105
|
-
|
147
|
+
const files = [];
|
148
|
+
stream.on('data', (evt) => {
|
149
|
+
const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
|
150
|
+
if (file) {
|
151
|
+
files.push(file);
|
152
|
+
}
|
153
|
+
});
|
154
|
+
await stream.waitForDone();
|
155
|
+
return files;
|
106
156
|
});
|
107
|
-
|
157
|
+
const fileChunks = await Promise.all(promises);
|
158
|
+
return fileChunks.flat();
|
108
159
|
}
|
109
160
|
/**
|
110
161
|
* Converts the generated files to a format that can be sent to the AI
|
@@ -143,7 +194,7 @@ class StormCodegen {
|
|
143
194
|
/**
|
144
195
|
* Generates the code using codegen for a given block.
|
145
196
|
*/
|
146
|
-
async generateBlock(yamlContent
|
197
|
+
async generateBlock(yamlContent) {
|
147
198
|
if (!yamlContent.spec.target?.kind) {
|
148
199
|
//Not all block types have targets
|
149
200
|
return;
|
@@ -153,7 +204,6 @@ class StormCodegen {
|
|
153
204
|
}
|
154
205
|
const codeGenerator = new codegen_1.BlockCodeGenerator(yamlContent);
|
155
206
|
codeGenerator.withOption('AIContext', stormClient_1.STORM_ID);
|
156
|
-
codeGenerator.withOption('AIScreens', screens ?? []);
|
157
207
|
return codeGenerator.generate();
|
158
208
|
}
|
159
209
|
}
|
@@ -2,13 +2,13 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import {
|
5
|
+
import { StormEvent } from './events';
|
6
6
|
import { BlockDefinition, Plan } from '@kapeta/schemas';
|
7
7
|
import { KapetaURI } from '@kapeta/nodejs-utils';
|
8
8
|
export interface BlockDefinitionInfo {
|
9
9
|
uri: KapetaURI;
|
10
10
|
content: BlockDefinition;
|
11
|
-
|
11
|
+
aiName: string;
|
12
12
|
}
|
13
13
|
export interface ParsedResult {
|
14
14
|
plan: Plan;
|
@@ -106,7 +106,6 @@ class StormEventParser {
|
|
106
106
|
this.connections = [];
|
107
107
|
}
|
108
108
|
addEvent(evt) {
|
109
|
-
console.log('Processing storm event', evt);
|
110
109
|
this.events.push(evt);
|
111
110
|
switch (evt.type) {
|
112
111
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -119,7 +118,6 @@ class StormEventParser {
|
|
119
118
|
apis: [],
|
120
119
|
models: [],
|
121
120
|
types: [],
|
122
|
-
screens: [],
|
123
121
|
};
|
124
122
|
break;
|
125
123
|
case 'PLAN_RETRY':
|
@@ -141,18 +139,9 @@ class StormEventParser {
|
|
141
139
|
case 'CREATE_CONNECTION':
|
142
140
|
this.connections.push(evt.payload);
|
143
141
|
break;
|
144
|
-
case 'SCREEN':
|
145
|
-
this.blocks[evt.payload.blockName].screens.push({
|
146
|
-
name: evt.payload.name,
|
147
|
-
description: evt.payload.description,
|
148
|
-
url: evt.payload.url,
|
149
|
-
template: evt.payload.template,
|
150
|
-
});
|
151
|
-
break;
|
152
142
|
default:
|
153
143
|
case 'SCREEN_CANDIDATE':
|
154
144
|
case 'FILE':
|
155
|
-
console.warn('Unhandled event: %s', evt.type, evt);
|
156
145
|
break;
|
157
146
|
}
|
158
147
|
}
|
@@ -190,15 +179,6 @@ class StormEventParser {
|
|
190
179
|
},
|
191
180
|
};
|
192
181
|
});
|
193
|
-
Object.values(this.blocks).forEach((blockInfo) => {
|
194
|
-
const blockRef = this.toRef(handle, blockInfo.name);
|
195
|
-
const block = blockDefinitions[blockRef.toNormalizedString()];
|
196
|
-
if (!block) {
|
197
|
-
console.warn('Block not found: %s', blockInfo.name);
|
198
|
-
return;
|
199
|
-
}
|
200
|
-
screens[blockRef.fullName] = blockInfo.screens;
|
201
|
-
});
|
202
182
|
// Copy API methods from API provider to CLIENT consumer
|
203
183
|
this.connections
|
204
184
|
.filter((connection) => connection.fromResourceType === 'API' && connection.toResourceType === 'CLIENT')
|
@@ -207,29 +187,28 @@ class StormEventParser {
|
|
207
187
|
const clientConsumerRef = this.toRef(handle, apiConnection.toComponent);
|
208
188
|
const apiProviderBlock = blockDefinitions[apiProviderRef.toNormalizedString()];
|
209
189
|
if (!apiProviderBlock) {
|
210
|
-
console.warn('API provider not found: %s', apiConnection.fromComponent);
|
190
|
+
console.warn('API provider not found: %s', apiConnection.fromComponent, apiConnection);
|
211
191
|
return;
|
212
192
|
}
|
213
193
|
const clientConsumerBlock = blockDefinitions[clientConsumerRef.toNormalizedString()];
|
214
194
|
if (!clientConsumerBlock) {
|
215
|
-
console.warn('Client consumer not found: %s', apiConnection.toComponent);
|
195
|
+
console.warn('Client consumer not found: %s', apiConnection.toComponent, apiConnection);
|
216
196
|
return;
|
217
197
|
}
|
218
198
|
const apiResource = apiProviderBlock.content.spec.providers?.find((p) => p.kind === this.options.apiKind && p.metadata.name === apiConnection.fromResource);
|
219
199
|
if (!apiResource) {
|
220
|
-
console.warn('API resource not found: %s on %s', apiConnection.fromResource, apiProviderRef.toNormalizedString());
|
200
|
+
console.warn('API resource not found: %s on %s', apiConnection.fromResource, apiProviderRef.toNormalizedString(), apiConnection);
|
221
201
|
return;
|
222
202
|
}
|
223
203
|
const clientResource = clientConsumerBlock.content.spec.consumers?.find((clientResource) => {
|
224
204
|
if (clientResource.kind !== this.options.clientKind) {
|
225
|
-
|
226
|
-
|
227
|
-
if (clientResource.metadata.name !== apiConnection.toResource) {
|
228
|
-
return;
|
205
|
+
console.warn('Client resource kind mismatch: %s', clientResource.kind, this.options.clientKind);
|
206
|
+
return false;
|
229
207
|
}
|
208
|
+
return clientResource.metadata.name === apiConnection.toResource;
|
230
209
|
});
|
231
210
|
if (!clientResource) {
|
232
|
-
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString());
|
211
|
+
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
|
233
212
|
return;
|
234
213
|
}
|
235
214
|
clientResource.spec.methods = apiResource.spec.methods;
|
@@ -284,6 +263,7 @@ class StormEventParser {
|
|
284
263
|
const blockRef = this.toRef(handle, blockInfo.name);
|
285
264
|
const blockDefinitionInfo = {
|
286
265
|
uri: blockRef,
|
266
|
+
aiName: blockInfo.name,
|
287
267
|
content: {
|
288
268
|
kind: this.toBlockKind(blockInfo.type),
|
289
269
|
metadata: {
|
@@ -305,7 +285,6 @@ class StormEventParser {
|
|
305
285
|
consumers: [],
|
306
286
|
},
|
307
287
|
},
|
308
|
-
screens: blockInfo.screens,
|
309
288
|
};
|
310
289
|
const blockSpec = blockDefinitionInfo.content.spec;
|
311
290
|
let apiResource = undefined;
|
@@ -21,7 +21,6 @@ router.post('/:handle/all', async (req, res) => {
|
|
21
21
|
try {
|
22
22
|
const stormOptions = await (0, event_parser_1.resolveOptions)();
|
23
23
|
const eventParser = new event_parser_1.StormEventParser(stormOptions);
|
24
|
-
console.log('Got prompt', req.stringBody);
|
25
24
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
26
25
|
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
27
26
|
res.set('Content-Type', 'application/x-ndjson');
|
@@ -41,8 +40,7 @@ router.post('/:handle/all', async (req, res) => {
|
|
41
40
|
return;
|
42
41
|
}
|
43
42
|
const result = eventParser.toResult(handle);
|
44
|
-
|
45
|
-
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks);
|
43
|
+
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
46
44
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
47
45
|
await stormCodegen.process();
|
48
46
|
await codegenStream;
|
@@ -52,26 +50,6 @@ router.post('/:handle/all', async (req, res) => {
|
|
52
50
|
sendError(err, res);
|
53
51
|
}
|
54
52
|
});
|
55
|
-
router.post('/metadata', async (req, res) => {
|
56
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
57
|
-
const result = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
58
|
-
await streamStormResponse(result, res);
|
59
|
-
});
|
60
|
-
router.post('/services/implement', async (req, res) => {
|
61
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
62
|
-
const result = await stormClient_1.stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
|
63
|
-
await streamStormResponse(result, res);
|
64
|
-
});
|
65
|
-
router.post('/ui/implement', async (req, res) => {
|
66
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
67
|
-
const result = await stormClient_1.stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
|
68
|
-
await streamStormResponse(result, res);
|
69
|
-
});
|
70
|
-
async function streamStormResponse(result, res) {
|
71
|
-
res.set('Content-Type', 'application/x-ndjson');
|
72
|
-
await streamStormPartialResponse(result, res);
|
73
|
-
sendDone(res);
|
74
|
-
}
|
75
53
|
function sendDone(res) {
|
76
54
|
res.write(JSON.stringify({
|
77
55
|
type: 'DONE',
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ConversationItem, StormFileImplementationPrompt, StormStream } from './stream';
|
1
|
+
import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
|
2
2
|
export declare const STORM_ID = "storm";
|
3
3
|
declare class StormClient {
|
4
4
|
private readonly _baseUrl;
|
@@ -6,7 +6,7 @@ declare class StormClient {
|
|
6
6
|
private createOptions;
|
7
7
|
private send;
|
8
8
|
createMetadata(prompt: string, history?: ConversationItem[]): Promise<StormStream>;
|
9
|
-
createUIImplementation(prompt:
|
9
|
+
createUIImplementation(prompt: StormUIImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
10
10
|
createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
11
11
|
}
|
12
12
|
export declare const stormClient: StormClient;
|
@@ -53,12 +53,6 @@ class StormClient {
|
|
53
53
|
jsonLStream.on('error', (error) => {
|
54
54
|
out.emit('error', error);
|
55
55
|
});
|
56
|
-
jsonLStream.on('pause', () => {
|
57
|
-
console.log('paused');
|
58
|
-
});
|
59
|
-
jsonLStream.on('resume', () => {
|
60
|
-
console.log('resumed');
|
61
|
-
});
|
62
56
|
jsonLStream.on('close', () => {
|
63
57
|
out.end();
|
64
58
|
});
|
@@ -77,7 +71,6 @@ class StormClient {
|
|
77
71
|
});
|
78
72
|
}
|
79
73
|
createServiceImplementation(prompt, history) {
|
80
|
-
console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
|
81
74
|
return this.send('/v2/services/merge', {
|
82
75
|
history: history ?? [],
|
83
76
|
prompt,
|
@@ -36,3 +36,10 @@ export interface StormFileImplementationPrompt {
|
|
36
36
|
template: StormFileInfo;
|
37
37
|
prompt: string;
|
38
38
|
}
|
39
|
+
export interface StormUIImplementationPrompt {
|
40
|
+
events: StormEvent[];
|
41
|
+
templates: StormFileInfo[];
|
42
|
+
context: StormFileInfo[];
|
43
|
+
blockName: string;
|
44
|
+
prompt: string;
|
45
|
+
}
|
@@ -2,15 +2,19 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
+
import { StormEvent } from './events';
|
5
6
|
import { BlockDefinitionInfo } from './event-parser';
|
6
7
|
import { StormStream } from './stream';
|
7
8
|
export declare class StormCodegen {
|
8
9
|
private readonly userPrompt;
|
9
10
|
private readonly blocks;
|
10
11
|
private readonly out;
|
11
|
-
|
12
|
+
private readonly events;
|
13
|
+
constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
|
12
14
|
process(): Promise<void>;
|
13
15
|
getStream(): StormStream;
|
16
|
+
private handleTemplateFileOutput;
|
17
|
+
private handleUiOutput;
|
14
18
|
private handleFileOutput;
|
15
19
|
/**
|
16
20
|
* Generates the code for a block and sends it to the AI
|
@@ -13,12 +13,13 @@ class StormCodegen {
|
|
13
13
|
userPrompt;
|
14
14
|
blocks;
|
15
15
|
out = new stream_1.StormStream();
|
16
|
-
|
16
|
+
events;
|
17
|
+
constructor(userPrompt, blocks, events) {
|
17
18
|
this.userPrompt = userPrompt;
|
18
19
|
this.blocks = blocks;
|
20
|
+
this.events = events;
|
19
21
|
}
|
20
22
|
async process() {
|
21
|
-
console.log('Processing blocks', this.blocks.length);
|
22
23
|
for (const block of this.blocks) {
|
23
24
|
await this.processBlockCode(block);
|
24
25
|
}
|
@@ -27,13 +28,42 @@ class StormCodegen {
|
|
27
28
|
getStream() {
|
28
29
|
return this.out;
|
29
30
|
}
|
30
|
-
|
31
|
+
handleTemplateFileOutput(blockUri, template, data) {
|
31
32
|
switch (data.type) {
|
32
33
|
case 'FILE':
|
33
34
|
template.filename = data.payload.filename;
|
34
35
|
template.content = data.payload.content;
|
36
|
+
return this.handleFileOutput(blockUri, data);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
handleUiOutput(blockUri, data) {
|
40
|
+
switch (data.type) {
|
41
|
+
case 'SCREEN':
|
42
|
+
this.out.emit('data', {
|
43
|
+
type: 'SCREEN',
|
44
|
+
reason: data.reason,
|
45
|
+
created: Date.now(),
|
46
|
+
payload: {
|
47
|
+
...data.payload,
|
48
|
+
blockName: blockUri.toNormalizedString(),
|
49
|
+
},
|
50
|
+
});
|
51
|
+
case 'FILE':
|
52
|
+
return this.handleFileOutput(blockUri, data);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
handleFileOutput(blockUri, data) {
|
56
|
+
switch (data.type) {
|
57
|
+
case 'FILE':
|
35
58
|
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
36
|
-
|
59
|
+
return {
|
60
|
+
type: 'FILE',
|
61
|
+
created: Date.now(),
|
62
|
+
payload: {
|
63
|
+
filename: data.payload.filename,
|
64
|
+
content: data.payload.content,
|
65
|
+
},
|
66
|
+
};
|
37
67
|
}
|
38
68
|
}
|
39
69
|
/**
|
@@ -41,22 +71,35 @@ class StormCodegen {
|
|
41
71
|
*/
|
42
72
|
async processBlockCode(block) {
|
43
73
|
// Generate the code for the block using the standard codegen templates
|
44
|
-
const generatedResult = await this.generateBlock(block.content
|
74
|
+
const generatedResult = await this.generateBlock(block.content);
|
45
75
|
if (!generatedResult) {
|
46
|
-
console.warn('No generated result for block', block.uri);
|
47
76
|
return;
|
48
77
|
}
|
49
78
|
const allFiles = this.toStormFiles(generatedResult);
|
50
79
|
// Send all the non-ai files to the stream
|
51
80
|
this.emitFiles(block.uri, allFiles);
|
52
|
-
const
|
53
|
-
|
54
|
-
|
81
|
+
const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
|
82
|
+
const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
|
83
|
+
if (uiTemplates.length > 0) {
|
84
|
+
const uiStream = await stormClient_1.stormClient.createUIImplementation({
|
85
|
+
events: this.events,
|
86
|
+
templates: uiTemplates,
|
87
|
+
context: relevantFiles,
|
88
|
+
blockName: block.aiName,
|
89
|
+
prompt: this.userPrompt,
|
90
|
+
});
|
91
|
+
uiStream.on('data', (evt) => {
|
92
|
+
this.handleUiOutput(block.uri, evt);
|
93
|
+
});
|
94
|
+
await uiStream.waitForDone();
|
95
|
+
}
|
96
|
+
// Gather the context files for implementation. These will be all be passed to the AI
|
97
|
+
const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
|
55
98
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
56
99
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
57
|
-
|
58
|
-
|
59
|
-
|
100
|
+
if (serviceFiles.length > 0) {
|
101
|
+
await this.processTemplates(block.uri, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
102
|
+
}
|
60
103
|
}
|
61
104
|
/**
|
62
105
|
* Emits the text-based files to the stream
|
@@ -94,17 +137,25 @@ class StormCodegen {
|
|
94
137
|
/**
|
95
138
|
* Sends the template to the AI and processes the response
|
96
139
|
*/
|
97
|
-
processTemplates(blockUri, generator, templates, contextFiles) {
|
140
|
+
async processTemplates(blockUri, generator, templates, contextFiles) {
|
98
141
|
const promises = templates.map(async (templateFile) => {
|
99
142
|
const stream = await generator({
|
100
143
|
context: contextFiles,
|
101
144
|
template: templateFile,
|
102
145
|
prompt: this.userPrompt,
|
103
146
|
});
|
104
|
-
|
105
|
-
|
147
|
+
const files = [];
|
148
|
+
stream.on('data', (evt) => {
|
149
|
+
const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
|
150
|
+
if (file) {
|
151
|
+
files.push(file);
|
152
|
+
}
|
153
|
+
});
|
154
|
+
await stream.waitForDone();
|
155
|
+
return files;
|
106
156
|
});
|
107
|
-
|
157
|
+
const fileChunks = await Promise.all(promises);
|
158
|
+
return fileChunks.flat();
|
108
159
|
}
|
109
160
|
/**
|
110
161
|
* Converts the generated files to a format that can be sent to the AI
|
@@ -143,7 +194,7 @@ class StormCodegen {
|
|
143
194
|
/**
|
144
195
|
* Generates the code using codegen for a given block.
|
145
196
|
*/
|
146
|
-
async generateBlock(yamlContent
|
197
|
+
async generateBlock(yamlContent) {
|
147
198
|
if (!yamlContent.spec.target?.kind) {
|
148
199
|
//Not all block types have targets
|
149
200
|
return;
|
@@ -153,7 +204,6 @@ class StormCodegen {
|
|
153
204
|
}
|
154
205
|
const codeGenerator = new codegen_1.BlockCodeGenerator(yamlContent);
|
155
206
|
codeGenerator.withOption('AIContext', stormClient_1.STORM_ID);
|
156
|
-
codeGenerator.withOption('AIScreens', screens ?? []);
|
157
207
|
return codeGenerator.generate();
|
158
208
|
}
|
159
209
|
}
|
@@ -2,13 +2,13 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import {
|
5
|
+
import { StormEvent } from './events';
|
6
6
|
import { BlockDefinition, Plan } from '@kapeta/schemas';
|
7
7
|
import { KapetaURI } from '@kapeta/nodejs-utils';
|
8
8
|
export interface BlockDefinitionInfo {
|
9
9
|
uri: KapetaURI;
|
10
10
|
content: BlockDefinition;
|
11
|
-
|
11
|
+
aiName: string;
|
12
12
|
}
|
13
13
|
export interface ParsedResult {
|
14
14
|
plan: Plan;
|
@@ -106,7 +106,6 @@ class StormEventParser {
|
|
106
106
|
this.connections = [];
|
107
107
|
}
|
108
108
|
addEvent(evt) {
|
109
|
-
console.log('Processing storm event', evt);
|
110
109
|
this.events.push(evt);
|
111
110
|
switch (evt.type) {
|
112
111
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -119,7 +118,6 @@ class StormEventParser {
|
|
119
118
|
apis: [],
|
120
119
|
models: [],
|
121
120
|
types: [],
|
122
|
-
screens: [],
|
123
121
|
};
|
124
122
|
break;
|
125
123
|
case 'PLAN_RETRY':
|
@@ -141,18 +139,9 @@ class StormEventParser {
|
|
141
139
|
case 'CREATE_CONNECTION':
|
142
140
|
this.connections.push(evt.payload);
|
143
141
|
break;
|
144
|
-
case 'SCREEN':
|
145
|
-
this.blocks[evt.payload.blockName].screens.push({
|
146
|
-
name: evt.payload.name,
|
147
|
-
description: evt.payload.description,
|
148
|
-
url: evt.payload.url,
|
149
|
-
template: evt.payload.template,
|
150
|
-
});
|
151
|
-
break;
|
152
142
|
default:
|
153
143
|
case 'SCREEN_CANDIDATE':
|
154
144
|
case 'FILE':
|
155
|
-
console.warn('Unhandled event: %s', evt.type, evt);
|
156
145
|
break;
|
157
146
|
}
|
158
147
|
}
|
@@ -190,15 +179,6 @@ class StormEventParser {
|
|
190
179
|
},
|
191
180
|
};
|
192
181
|
});
|
193
|
-
Object.values(this.blocks).forEach((blockInfo) => {
|
194
|
-
const blockRef = this.toRef(handle, blockInfo.name);
|
195
|
-
const block = blockDefinitions[blockRef.toNormalizedString()];
|
196
|
-
if (!block) {
|
197
|
-
console.warn('Block not found: %s', blockInfo.name);
|
198
|
-
return;
|
199
|
-
}
|
200
|
-
screens[blockRef.fullName] = blockInfo.screens;
|
201
|
-
});
|
202
182
|
// Copy API methods from API provider to CLIENT consumer
|
203
183
|
this.connections
|
204
184
|
.filter((connection) => connection.fromResourceType === 'API' && connection.toResourceType === 'CLIENT')
|
@@ -207,29 +187,28 @@ class StormEventParser {
|
|
207
187
|
const clientConsumerRef = this.toRef(handle, apiConnection.toComponent);
|
208
188
|
const apiProviderBlock = blockDefinitions[apiProviderRef.toNormalizedString()];
|
209
189
|
if (!apiProviderBlock) {
|
210
|
-
console.warn('API provider not found: %s', apiConnection.fromComponent);
|
190
|
+
console.warn('API provider not found: %s', apiConnection.fromComponent, apiConnection);
|
211
191
|
return;
|
212
192
|
}
|
213
193
|
const clientConsumerBlock = blockDefinitions[clientConsumerRef.toNormalizedString()];
|
214
194
|
if (!clientConsumerBlock) {
|
215
|
-
console.warn('Client consumer not found: %s', apiConnection.toComponent);
|
195
|
+
console.warn('Client consumer not found: %s', apiConnection.toComponent, apiConnection);
|
216
196
|
return;
|
217
197
|
}
|
218
198
|
const apiResource = apiProviderBlock.content.spec.providers?.find((p) => p.kind === this.options.apiKind && p.metadata.name === apiConnection.fromResource);
|
219
199
|
if (!apiResource) {
|
220
|
-
console.warn('API resource not found: %s on %s', apiConnection.fromResource, apiProviderRef.toNormalizedString());
|
200
|
+
console.warn('API resource not found: %s on %s', apiConnection.fromResource, apiProviderRef.toNormalizedString(), apiConnection);
|
221
201
|
return;
|
222
202
|
}
|
223
203
|
const clientResource = clientConsumerBlock.content.spec.consumers?.find((clientResource) => {
|
224
204
|
if (clientResource.kind !== this.options.clientKind) {
|
225
|
-
|
226
|
-
|
227
|
-
if (clientResource.metadata.name !== apiConnection.toResource) {
|
228
|
-
return;
|
205
|
+
console.warn('Client resource kind mismatch: %s', clientResource.kind, this.options.clientKind);
|
206
|
+
return false;
|
229
207
|
}
|
208
|
+
return clientResource.metadata.name === apiConnection.toResource;
|
230
209
|
});
|
231
210
|
if (!clientResource) {
|
232
|
-
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString());
|
211
|
+
console.warn('Client resource not found: %s on %s', apiConnection.toResource, clientConsumerRef.toNormalizedString(), apiConnection);
|
233
212
|
return;
|
234
213
|
}
|
235
214
|
clientResource.spec.methods = apiResource.spec.methods;
|
@@ -284,6 +263,7 @@ class StormEventParser {
|
|
284
263
|
const blockRef = this.toRef(handle, blockInfo.name);
|
285
264
|
const blockDefinitionInfo = {
|
286
265
|
uri: blockRef,
|
266
|
+
aiName: blockInfo.name,
|
287
267
|
content: {
|
288
268
|
kind: this.toBlockKind(blockInfo.type),
|
289
269
|
metadata: {
|
@@ -305,7 +285,6 @@ class StormEventParser {
|
|
305
285
|
consumers: [],
|
306
286
|
},
|
307
287
|
},
|
308
|
-
screens: blockInfo.screens,
|
309
288
|
};
|
310
289
|
const blockSpec = blockDefinitionInfo.content.spec;
|
311
290
|
let apiResource = undefined;
|
@@ -21,7 +21,6 @@ router.post('/:handle/all', async (req, res) => {
|
|
21
21
|
try {
|
22
22
|
const stormOptions = await (0, event_parser_1.resolveOptions)();
|
23
23
|
const eventParser = new event_parser_1.StormEventParser(stormOptions);
|
24
|
-
console.log('Got prompt', req.stringBody);
|
25
24
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
26
25
|
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
27
26
|
res.set('Content-Type', 'application/x-ndjson');
|
@@ -41,8 +40,7 @@ router.post('/:handle/all', async (req, res) => {
|
|
41
40
|
return;
|
42
41
|
}
|
43
42
|
const result = eventParser.toResult(handle);
|
44
|
-
|
45
|
-
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks);
|
43
|
+
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
46
44
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
47
45
|
await stormCodegen.process();
|
48
46
|
await codegenStream;
|
@@ -52,26 +50,6 @@ router.post('/:handle/all', async (req, res) => {
|
|
52
50
|
sendError(err, res);
|
53
51
|
}
|
54
52
|
});
|
55
|
-
router.post('/metadata', async (req, res) => {
|
56
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
57
|
-
const result = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
58
|
-
await streamStormResponse(result, res);
|
59
|
-
});
|
60
|
-
router.post('/services/implement', async (req, res) => {
|
61
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
62
|
-
const result = await stormClient_1.stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
|
63
|
-
await streamStormResponse(result, res);
|
64
|
-
});
|
65
|
-
router.post('/ui/implement', async (req, res) => {
|
66
|
-
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
67
|
-
const result = await stormClient_1.stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
|
68
|
-
await streamStormResponse(result, res);
|
69
|
-
});
|
70
|
-
async function streamStormResponse(result, res) {
|
71
|
-
res.set('Content-Type', 'application/x-ndjson');
|
72
|
-
await streamStormPartialResponse(result, res);
|
73
|
-
sendDone(res);
|
74
|
-
}
|
75
53
|
function sendDone(res) {
|
76
54
|
res.write(JSON.stringify({
|
77
55
|
type: 'DONE',
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ConversationItem, StormFileImplementationPrompt, StormStream } from './stream';
|
1
|
+
import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
|
2
2
|
export declare const STORM_ID = "storm";
|
3
3
|
declare class StormClient {
|
4
4
|
private readonly _baseUrl;
|
@@ -6,7 +6,7 @@ declare class StormClient {
|
|
6
6
|
private createOptions;
|
7
7
|
private send;
|
8
8
|
createMetadata(prompt: string, history?: ConversationItem[]): Promise<StormStream>;
|
9
|
-
createUIImplementation(prompt:
|
9
|
+
createUIImplementation(prompt: StormUIImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
10
10
|
createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
11
11
|
}
|
12
12
|
export declare const stormClient: StormClient;
|
@@ -53,12 +53,6 @@ class StormClient {
|
|
53
53
|
jsonLStream.on('error', (error) => {
|
54
54
|
out.emit('error', error);
|
55
55
|
});
|
56
|
-
jsonLStream.on('pause', () => {
|
57
|
-
console.log('paused');
|
58
|
-
});
|
59
|
-
jsonLStream.on('resume', () => {
|
60
|
-
console.log('resumed');
|
61
|
-
});
|
62
56
|
jsonLStream.on('close', () => {
|
63
57
|
out.end();
|
64
58
|
});
|
@@ -77,7 +71,6 @@ class StormClient {
|
|
77
71
|
});
|
78
72
|
}
|
79
73
|
createServiceImplementation(prompt, history) {
|
80
|
-
console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
|
81
74
|
return this.send('/v2/services/merge', {
|
82
75
|
history: history ?? [],
|
83
76
|
prompt,
|
@@ -36,3 +36,10 @@ export interface StormFileImplementationPrompt {
|
|
36
36
|
template: StormFileInfo;
|
37
37
|
prompt: string;
|
38
38
|
}
|
39
|
+
export interface StormUIImplementationPrompt {
|
40
|
+
events: StormEvent[];
|
41
|
+
templates: StormFileInfo[];
|
42
|
+
context: StormFileInfo[];
|
43
|
+
blockName: string;
|
44
|
+
prompt: string;
|
45
|
+
}
|
package/package.json
CHANGED
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 { ScreenTemplate, StormEvent, StormEventFile } from './events';
|
11
|
+
import { ScreenTemplate, StormEvent, StormEventFile, StormEventScreen } 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';
|
@@ -22,15 +22,15 @@ export class StormCodegen {
|
|
22
22
|
private readonly userPrompt: string;
|
23
23
|
private readonly blocks: BlockDefinitionInfo[];
|
24
24
|
private readonly out = new StormStream();
|
25
|
+
private readonly events: StormEvent[];
|
25
26
|
|
26
|
-
constructor(userPrompt: string, blocks: BlockDefinitionInfo[]) {
|
27
|
+
constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
27
28
|
this.userPrompt = userPrompt;
|
28
29
|
this.blocks = blocks;
|
30
|
+
this.events = events;
|
29
31
|
}
|
30
32
|
|
31
33
|
public async process() {
|
32
|
-
console.log('Processing blocks', this.blocks.length);
|
33
|
-
|
34
34
|
for (const block of this.blocks) {
|
35
35
|
await this.processBlockCode(block);
|
36
36
|
}
|
@@ -42,13 +42,44 @@ export class StormCodegen {
|
|
42
42
|
return this.out;
|
43
43
|
}
|
44
44
|
|
45
|
-
private
|
45
|
+
private handleTemplateFileOutput(blockUri: KapetaURI, 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);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
private handleUiOutput(blockUri: KapetaURI, data: StormEvent) {
|
55
|
+
switch (data.type) {
|
56
|
+
case 'SCREEN':
|
57
|
+
this.out.emit('data', {
|
58
|
+
type: 'SCREEN',
|
59
|
+
reason: data.reason,
|
60
|
+
created: Date.now(),
|
61
|
+
payload: {
|
62
|
+
...data.payload,
|
63
|
+
blockName: blockUri.toNormalizedString(),
|
64
|
+
},
|
65
|
+
});
|
66
|
+
case 'FILE':
|
67
|
+
return this.handleFileOutput(blockUri, data);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
private handleFileOutput(blockUri: KapetaURI, data: StormEvent) {
|
72
|
+
switch (data.type) {
|
73
|
+
case 'FILE':
|
50
74
|
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
51
|
-
|
75
|
+
return {
|
76
|
+
type: 'FILE',
|
77
|
+
created: Date.now(),
|
78
|
+
payload: {
|
79
|
+
filename: data.payload.filename,
|
80
|
+
content: data.payload.content,
|
81
|
+
},
|
82
|
+
} as StormEventFile;
|
52
83
|
}
|
53
84
|
}
|
54
85
|
|
@@ -57,9 +88,8 @@ export class StormCodegen {
|
|
57
88
|
*/
|
58
89
|
private async processBlockCode(block: BlockDefinitionInfo) {
|
59
90
|
// Generate the code for the block using the standard codegen templates
|
60
|
-
const generatedResult = await this.generateBlock(block.content
|
91
|
+
const generatedResult = await this.generateBlock(block.content);
|
61
92
|
if (!generatedResult) {
|
62
|
-
console.warn('No generated result for block', block.uri);
|
63
93
|
return;
|
64
94
|
}
|
65
95
|
|
@@ -68,24 +98,41 @@ export class StormCodegen {
|
|
68
98
|
// Send all the non-ai files to the stream
|
69
99
|
this.emitFiles(block.uri, allFiles);
|
70
100
|
|
71
|
-
const
|
101
|
+
const relevantFiles: StormFileInfo[] = allFiles.filter(
|
102
|
+
(file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
|
103
|
+
);
|
104
|
+
const uiTemplates: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
|
105
|
+
if (uiTemplates.length > 0) {
|
106
|
+
const uiStream = await stormClient.createUIImplementation({
|
107
|
+
events: this.events,
|
108
|
+
templates: uiTemplates,
|
109
|
+
context: relevantFiles,
|
110
|
+
blockName: block.aiName,
|
111
|
+
prompt: this.userPrompt,
|
112
|
+
});
|
113
|
+
|
114
|
+
uiStream.on('data', (evt) => {
|
115
|
+
this.handleUiOutput(block.uri, evt);
|
116
|
+
});
|
117
|
+
|
118
|
+
await uiStream.waitForDone();
|
119
|
+
}
|
72
120
|
|
73
|
-
// Gather the context files. These will be all be passed to the AI
|
74
|
-
const contextFiles: StormFileInfo[] =
|
75
|
-
(file) =>
|
121
|
+
// Gather the context files for implementation. These will be all be passed to the AI
|
122
|
+
const contextFiles: StormFileInfo[] = relevantFiles.filter(
|
123
|
+
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
|
76
124
|
);
|
77
125
|
|
78
126
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
79
127
|
const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
//await this.processTemplates(stormClient.createUIImplementation, uiTemplates, contextFiles);
|
128
|
+
if (serviceFiles.length > 0) {
|
129
|
+
await this.processTemplates(
|
130
|
+
block.uri,
|
131
|
+
stormClient.createServiceImplementation.bind(stormClient),
|
132
|
+
serviceFiles,
|
133
|
+
contextFiles
|
134
|
+
);
|
135
|
+
}
|
89
136
|
}
|
90
137
|
|
91
138
|
/**
|
@@ -129,7 +176,7 @@ export class StormCodegen {
|
|
129
176
|
/**
|
130
177
|
* Sends the template to the AI and processes the response
|
131
178
|
*/
|
132
|
-
private processTemplates(
|
179
|
+
private async processTemplates(
|
133
180
|
blockUri: KapetaURI,
|
134
181
|
generator: ImplementationGenerator,
|
135
182
|
templates: StormFileInfo[],
|
@@ -142,12 +189,22 @@ export class StormCodegen {
|
|
142
189
|
prompt: this.userPrompt,
|
143
190
|
});
|
144
191
|
|
145
|
-
|
192
|
+
const files: StormEventFile[] = [];
|
146
193
|
|
147
|
-
|
194
|
+
stream.on('data', (evt) => {
|
195
|
+
const file = this.handleTemplateFileOutput(blockUri, templateFile, evt);
|
196
|
+
if (file) {
|
197
|
+
files.push(file);
|
198
|
+
}
|
199
|
+
});
|
200
|
+
|
201
|
+
await stream.waitForDone();
|
202
|
+
return files;
|
148
203
|
});
|
149
204
|
|
150
|
-
|
205
|
+
const fileChunks = await Promise.all(promises);
|
206
|
+
|
207
|
+
return fileChunks.flat();
|
151
208
|
}
|
152
209
|
|
153
210
|
/**
|
@@ -191,7 +248,7 @@ export class StormCodegen {
|
|
191
248
|
/**
|
192
249
|
* Generates the code using codegen for a given block.
|
193
250
|
*/
|
194
|
-
private async generateBlock(yamlContent: Definition
|
251
|
+
private async generateBlock(yamlContent: Definition) {
|
195
252
|
if (!yamlContent.spec.target?.kind) {
|
196
253
|
//Not all block types have targets
|
197
254
|
return;
|
@@ -203,7 +260,6 @@ export class StormCodegen {
|
|
203
260
|
|
204
261
|
const codeGenerator = new BlockCodeGenerator(yamlContent as BlockDefinition);
|
205
262
|
codeGenerator.withOption('AIContext', STORM_ID);
|
206
|
-
codeGenerator.withOption('AIScreens', screens ?? []);
|
207
263
|
|
208
264
|
return codeGenerator.generate();
|
209
265
|
}
|
@@ -28,7 +28,7 @@ import { definitionsManager } from '../definitionsManager';
|
|
28
28
|
export interface BlockDefinitionInfo {
|
29
29
|
uri: KapetaURI;
|
30
30
|
content: BlockDefinition;
|
31
|
-
|
31
|
+
aiName: string;
|
32
32
|
}
|
33
33
|
|
34
34
|
export interface ParsedResult {
|
@@ -197,7 +197,6 @@ export class StormEventParser {
|
|
197
197
|
}
|
198
198
|
|
199
199
|
public addEvent(evt: StormEvent): void {
|
200
|
-
console.log('Processing storm event', evt);
|
201
200
|
this.events.push(evt);
|
202
201
|
switch (evt.type) {
|
203
202
|
case 'CREATE_PLAN_PROPERTIES':
|
@@ -210,7 +209,6 @@ export class StormEventParser {
|
|
210
209
|
apis: [],
|
211
210
|
models: [],
|
212
211
|
types: [],
|
213
|
-
screens: [],
|
214
212
|
};
|
215
213
|
break;
|
216
214
|
case 'PLAN_RETRY':
|
@@ -232,19 +230,10 @@ export class StormEventParser {
|
|
232
230
|
case 'CREATE_CONNECTION':
|
233
231
|
this.connections.push(evt.payload);
|
234
232
|
break;
|
235
|
-
case 'SCREEN':
|
236
|
-
this.blocks[evt.payload.blockName].screens.push({
|
237
|
-
name: evt.payload.name,
|
238
|
-
description: evt.payload.description,
|
239
|
-
url: evt.payload.url,
|
240
|
-
template: evt.payload.template,
|
241
|
-
});
|
242
|
-
break;
|
243
233
|
|
244
234
|
default:
|
245
235
|
case 'SCREEN_CANDIDATE':
|
246
236
|
case 'FILE':
|
247
|
-
console.warn('Unhandled event: %s', evt.type, evt);
|
248
237
|
break;
|
249
238
|
}
|
250
239
|
}
|
@@ -288,17 +277,6 @@ export class StormEventParser {
|
|
288
277
|
} satisfies BlockInstance;
|
289
278
|
});
|
290
279
|
|
291
|
-
Object.values(this.blocks).forEach((blockInfo) => {
|
292
|
-
const blockRef = this.toRef(handle, blockInfo.name);
|
293
|
-
const block = blockDefinitions[blockRef.toNormalizedString()];
|
294
|
-
if (!block) {
|
295
|
-
console.warn('Block not found: %s', blockInfo.name);
|
296
|
-
return;
|
297
|
-
}
|
298
|
-
|
299
|
-
screens[blockRef.fullName] = blockInfo.screens;
|
300
|
-
});
|
301
|
-
|
302
280
|
// Copy API methods from API provider to CLIENT consumer
|
303
281
|
this.connections
|
304
282
|
.filter((connection) => connection.fromResourceType === 'API' && connection.toResourceType === 'CLIENT')
|
@@ -307,12 +285,12 @@ export class StormEventParser {
|
|
307
285
|
const clientConsumerRef = this.toRef(handle, apiConnection.toComponent);
|
308
286
|
const apiProviderBlock = blockDefinitions[apiProviderRef.toNormalizedString()];
|
309
287
|
if (!apiProviderBlock) {
|
310
|
-
console.warn('API provider not found: %s', apiConnection.fromComponent);
|
288
|
+
console.warn('API provider not found: %s', apiConnection.fromComponent, apiConnection);
|
311
289
|
return;
|
312
290
|
}
|
313
291
|
const clientConsumerBlock = blockDefinitions[clientConsumerRef.toNormalizedString()];
|
314
292
|
if (!clientConsumerBlock) {
|
315
|
-
console.warn('Client consumer not found: %s', apiConnection.toComponent);
|
293
|
+
console.warn('Client consumer not found: %s', apiConnection.toComponent, apiConnection);
|
316
294
|
return;
|
317
295
|
}
|
318
296
|
|
@@ -324,25 +302,27 @@ export class StormEventParser {
|
|
324
302
|
console.warn(
|
325
303
|
'API resource not found: %s on %s',
|
326
304
|
apiConnection.fromResource,
|
327
|
-
apiProviderRef.toNormalizedString()
|
305
|
+
apiProviderRef.toNormalizedString(),
|
306
|
+
apiConnection
|
328
307
|
);
|
329
308
|
return;
|
330
309
|
}
|
331
310
|
|
332
311
|
const clientResource = clientConsumerBlock.content.spec.consumers?.find((clientResource) => {
|
333
312
|
if (clientResource.kind !== this.options.clientKind) {
|
334
|
-
|
335
|
-
|
336
|
-
if (clientResource.metadata.name !== apiConnection.toResource) {
|
337
|
-
return;
|
313
|
+
console.warn('Client resource kind mismatch: %s', clientResource.kind, this.options.clientKind);
|
314
|
+
return false;
|
338
315
|
}
|
316
|
+
|
317
|
+
return clientResource.metadata.name === apiConnection.toResource;
|
339
318
|
});
|
340
319
|
|
341
320
|
if (!clientResource) {
|
342
321
|
console.warn(
|
343
322
|
'Client resource not found: %s on %s',
|
344
323
|
apiConnection.toResource,
|
345
|
-
clientConsumerRef.toNormalizedString()
|
324
|
+
clientConsumerRef.toNormalizedString(),
|
325
|
+
apiConnection
|
346
326
|
);
|
347
327
|
return;
|
348
328
|
}
|
@@ -407,6 +387,7 @@ export class StormEventParser {
|
|
407
387
|
|
408
388
|
const blockDefinitionInfo: BlockDefinitionInfo = {
|
409
389
|
uri: blockRef,
|
390
|
+
aiName: blockInfo.name,
|
410
391
|
content: {
|
411
392
|
kind: this.toBlockKind(blockInfo.type),
|
412
393
|
metadata: {
|
@@ -428,7 +409,6 @@ export class StormEventParser {
|
|
428
409
|
consumers: [],
|
429
410
|
},
|
430
411
|
},
|
431
|
-
screens: blockInfo.screens,
|
432
412
|
};
|
433
413
|
|
434
414
|
const blockSpec = blockDefinitionInfo.content.spec;
|
package/src/storm/events.ts
CHANGED
package/src/storm/routes.ts
CHANGED
@@ -27,7 +27,6 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
27
27
|
|
28
28
|
const eventParser = new StormEventParser(stormOptions);
|
29
29
|
|
30
|
-
console.log('Got prompt', req.stringBody);
|
31
30
|
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
32
31
|
const metaStream = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
33
32
|
|
@@ -52,9 +51,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
52
51
|
}
|
53
52
|
const result = eventParser.toResult(handle);
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks);
|
54
|
+
const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
58
55
|
|
59
56
|
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
60
57
|
|
@@ -68,35 +65,6 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
68
65
|
}
|
69
66
|
});
|
70
67
|
|
71
|
-
router.post('/metadata', async (req: KapetaBodyRequest, res: Response) => {
|
72
|
-
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
73
|
-
const result = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
74
|
-
|
75
|
-
await streamStormResponse(result, res);
|
76
|
-
});
|
77
|
-
|
78
|
-
router.post('/services/implement', async (req: KapetaBodyRequest, res: Response) => {
|
79
|
-
const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
|
80
|
-
const result = await stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
|
81
|
-
|
82
|
-
await streamStormResponse(result, res);
|
83
|
-
});
|
84
|
-
|
85
|
-
router.post('/ui/implement', async (req: KapetaBodyRequest, res: Response) => {
|
86
|
-
const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
|
87
|
-
const result = await stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
|
88
|
-
|
89
|
-
await streamStormResponse(result, res);
|
90
|
-
});
|
91
|
-
|
92
|
-
async function streamStormResponse(result: StormStream, res: Response) {
|
93
|
-
res.set('Content-Type', 'application/x-ndjson');
|
94
|
-
|
95
|
-
await streamStormPartialResponse(result, res);
|
96
|
-
|
97
|
-
sendDone(res);
|
98
|
-
}
|
99
|
-
|
100
68
|
function sendDone(res: Response) {
|
101
69
|
res.write(
|
102
70
|
JSON.stringify({
|
package/src/storm/stormClient.ts
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
StormFileImplementationPrompt,
|
13
13
|
StormFileInfo,
|
14
14
|
StormStream,
|
15
|
+
StormUIImplementationPrompt,
|
15
16
|
} from './stream';
|
16
17
|
|
17
18
|
export const STORM_ID = 'storm';
|
@@ -73,14 +74,6 @@ class StormClient {
|
|
73
74
|
out.emit('error', error);
|
74
75
|
});
|
75
76
|
|
76
|
-
jsonLStream.on('pause', () => {
|
77
|
-
console.log('paused');
|
78
|
-
});
|
79
|
-
|
80
|
-
jsonLStream.on('resume', () => {
|
81
|
-
console.log('resumed');
|
82
|
-
});
|
83
|
-
|
84
77
|
jsonLStream.on('close', () => {
|
85
78
|
out.end();
|
86
79
|
});
|
@@ -95,15 +88,14 @@ class StormClient {
|
|
95
88
|
});
|
96
89
|
}
|
97
90
|
|
98
|
-
public createUIImplementation(prompt:
|
99
|
-
return this.send<
|
91
|
+
public createUIImplementation(prompt: StormUIImplementationPrompt, history?: ConversationItem[]) {
|
92
|
+
return this.send<StormUIImplementationPrompt>('/v2/ui/merge', {
|
100
93
|
history: history ?? [],
|
101
94
|
prompt,
|
102
95
|
});
|
103
96
|
}
|
104
97
|
|
105
98
|
public createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]) {
|
106
|
-
console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
|
107
99
|
return this.send<StormFileImplementationPrompt>('/v2/services/merge', {
|
108
100
|
history: history ?? [],
|
109
101
|
prompt,
|
package/src/storm/stream.ts
CHANGED
@@ -86,3 +86,11 @@ export interface StormFileImplementationPrompt {
|
|
86
86
|
template: StormFileInfo;
|
87
87
|
prompt: string;
|
88
88
|
}
|
89
|
+
|
90
|
+
export interface StormUIImplementationPrompt {
|
91
|
+
events: StormEvent[];
|
92
|
+
templates: StormFileInfo[];
|
93
|
+
context: StormFileInfo[];
|
94
|
+
blockName: string;
|
95
|
+
prompt: string;
|
96
|
+
}
|