@kapeta/local-cluster-service 0.49.0 → 0.50.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 +2 -0
- package/dist/cjs/src/storm/codegen.js +35 -2
- package/dist/cjs/src/storm/event-parser.d.ts +4 -1
- package/dist/cjs/src/storm/event-parser.js +21 -3
- package/dist/cjs/src/storm/events.d.ts +13 -1
- package/dist/cjs/src/storm/events.js +7 -0
- package/dist/cjs/src/storm/routes.js +66 -9
- package/dist/cjs/src/storm/stormClient.js +5 -1
- package/dist/cjs/src/storm/stream.d.ts +5 -0
- package/dist/cjs/src/storm/stream.js +11 -0
- package/dist/esm/src/storm/codegen.d.ts +2 -0
- package/dist/esm/src/storm/codegen.js +35 -2
- package/dist/esm/src/storm/event-parser.d.ts +4 -1
- package/dist/esm/src/storm/event-parser.js +21 -3
- package/dist/esm/src/storm/events.d.ts +13 -1
- package/dist/esm/src/storm/events.js +7 -0
- package/dist/esm/src/storm/routes.js +66 -9
- package/dist/esm/src/storm/stormClient.js +5 -1
- package/dist/esm/src/storm/stream.d.ts +5 -0
- package/dist/esm/src/storm/stream.js +11 -0
- package/package.json +1 -1
- package/src/storm/codegen.ts +43 -2
- package/src/storm/event-parser.ts +29 -3
- package/src/storm/events.ts +16 -1
- package/src/storm/routes.ts +87 -23
- package/src/storm/stormClient.ts +8 -1
- package/src/storm/stream.ts +15 -0
@@ -184,4 +184,16 @@ export interface StormEventDefinitionChange {
|
|
184
184
|
created: number;
|
185
185
|
payload: StormDefinitions;
|
186
186
|
}
|
187
|
-
export
|
187
|
+
export declare enum StormEventPhaseType {
|
188
|
+
META = "META",
|
189
|
+
DEFINITIONS = "DEFINITIONS",
|
190
|
+
IMPLEMENTATION = "IMPLEMENTATION"
|
191
|
+
}
|
192
|
+
export interface StormEventPhases {
|
193
|
+
type: 'PHASE_START' | 'PHASE_END';
|
194
|
+
created: number;
|
195
|
+
payload: {
|
196
|
+
phaseType: StormEventPhaseType;
|
197
|
+
};
|
198
|
+
}
|
199
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
|
@@ -1,2 +1,9 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.StormEventPhaseType = void 0;
|
4
|
+
var StormEventPhaseType;
|
5
|
+
(function (StormEventPhaseType) {
|
6
|
+
StormEventPhaseType["META"] = "META";
|
7
|
+
StormEventPhaseType["DEFINITIONS"] = "DEFINITIONS";
|
8
|
+
StormEventPhaseType["IMPLEMENTATION"] = "IMPLEMENTATION";
|
9
|
+
})(StormEventPhaseType || (exports.StormEventPhaseType = StormEventPhaseType = {}));
|
@@ -12,6 +12,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
12
|
const cors_1 = require("../middleware/cors");
|
13
13
|
const stringBody_1 = require("../middleware/stringBody");
|
14
14
|
const stormClient_1 = require("./stormClient");
|
15
|
+
const events_1 = require("./events");
|
15
16
|
const event_parser_1 = require("./event-parser");
|
16
17
|
const codegen_1 = require("./codegen");
|
17
18
|
const assetManager_1 = require("../assetManager");
|
@@ -27,15 +28,41 @@ router.post('/:handle/all', async (req, res) => {
|
|
27
28
|
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
|
28
29
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
29
30
|
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, conversationId);
|
31
|
+
onRequestAborted(req, res, () => {
|
32
|
+
metaStream.abort();
|
33
|
+
});
|
30
34
|
res.set('Content-Type', 'application/x-ndjson');
|
31
35
|
res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
|
32
36
|
res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
|
37
|
+
let currentPhase = events_1.StormEventPhaseType.META;
|
33
38
|
metaStream.on('data', (data) => {
|
34
39
|
const result = eventParser.processEvent(req.params.handle, data);
|
40
|
+
switch (data.type) {
|
41
|
+
case 'CREATE_API':
|
42
|
+
case 'CREATE_MODEL':
|
43
|
+
case 'CREATE_TYPE':
|
44
|
+
if (currentPhase !== events_1.StormEventPhaseType.DEFINITIONS) {
|
45
|
+
sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.META));
|
46
|
+
currentPhase = events_1.StormEventPhaseType.DEFINITIONS;
|
47
|
+
sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.DEFINITIONS));
|
48
|
+
}
|
49
|
+
break;
|
50
|
+
}
|
35
51
|
sendEvent(res, data);
|
36
52
|
sendDefinitions(res, result);
|
37
53
|
});
|
38
|
-
|
54
|
+
try {
|
55
|
+
sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.META));
|
56
|
+
await waitForStormStream(metaStream);
|
57
|
+
}
|
58
|
+
finally {
|
59
|
+
if (!metaStream.isAborted()) {
|
60
|
+
sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(currentPhase));
|
61
|
+
}
|
62
|
+
}
|
63
|
+
if (metaStream.isAborted()) {
|
64
|
+
return;
|
65
|
+
}
|
39
66
|
if (!eventParser.isValid()) {
|
40
67
|
// We can't continue if the meta stream is invalid
|
41
68
|
sendEvent(res, {
|
@@ -48,32 +75,45 @@ router.post('/:handle/all', async (req, res) => {
|
|
48
75
|
return;
|
49
76
|
}
|
50
77
|
const result = eventParser.toResult(handle);
|
78
|
+
if (metaStream.isAborted()) {
|
79
|
+
return;
|
80
|
+
}
|
51
81
|
sendDefinitions(res, result);
|
52
82
|
if (!req.query.skipCodegen) {
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
83
|
+
try {
|
84
|
+
sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
|
85
|
+
const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
|
86
|
+
onRequestAborted(req, res, () => {
|
87
|
+
stormCodegen.abort();
|
88
|
+
});
|
89
|
+
const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
|
90
|
+
await stormCodegen.process();
|
91
|
+
await codegenPromise;
|
92
|
+
}
|
93
|
+
finally {
|
94
|
+
if (!metaStream.isAborted()) {
|
95
|
+
sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
|
96
|
+
}
|
97
|
+
}
|
57
98
|
}
|
58
99
|
sendDone(res);
|
59
100
|
}
|
60
101
|
catch (err) {
|
61
102
|
sendError(err, res);
|
62
|
-
res.
|
103
|
+
if (!res.closed) {
|
104
|
+
res.end();
|
105
|
+
}
|
63
106
|
}
|
64
107
|
});
|
65
108
|
router.post('/block/create', async (req, res) => {
|
66
109
|
const createRequest = JSON.parse(req.stringBody ?? '{}');
|
67
110
|
try {
|
68
111
|
const ymlPath = path_1.default.join(createRequest.newPath, 'kapeta.yml');
|
69
|
-
console.log('Creating block at', ymlPath);
|
70
112
|
const [asset] = await assetManager_1.assetManager.createAsset(ymlPath, createRequest.definition);
|
71
113
|
if (await fs_extra_1.default.pathExists(createRequest.tmpPath)) {
|
72
|
-
console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
|
73
114
|
await fs_extra_1.default.move(createRequest.tmpPath, createRequest.newPath, {
|
74
115
|
overwrite: true,
|
75
116
|
});
|
76
|
-
console.log('Updating asset', asset.ref);
|
77
117
|
res.send(await assetManager_1.assetManager.updateAsset(asset.ref, createRequest.definition));
|
78
118
|
}
|
79
119
|
else {
|
@@ -93,6 +133,9 @@ function sendDefinitions(res, result) {
|
|
93
133
|
});
|
94
134
|
}
|
95
135
|
function sendDone(res) {
|
136
|
+
if (res.closed) {
|
137
|
+
return;
|
138
|
+
}
|
96
139
|
sendEvent(res, {
|
97
140
|
type: 'DONE',
|
98
141
|
created: Date.now(),
|
@@ -100,6 +143,9 @@ function sendDone(res) {
|
|
100
143
|
res.end();
|
101
144
|
}
|
102
145
|
function sendError(err, res) {
|
146
|
+
if (res.closed) {
|
147
|
+
return;
|
148
|
+
}
|
103
149
|
console.error('Failed to send prompt', err);
|
104
150
|
if (res.headersSent) {
|
105
151
|
sendEvent(res, {
|
@@ -137,6 +183,17 @@ function streamStormPartialResponse(result, res) {
|
|
137
183
|
});
|
138
184
|
}
|
139
185
|
function sendEvent(res, evt) {
|
186
|
+
if (res.closed) {
|
187
|
+
return;
|
188
|
+
}
|
140
189
|
res.write(JSON.stringify(evt) + '\n');
|
141
190
|
}
|
191
|
+
function onRequestAborted(req, res, onAborted) {
|
192
|
+
req.on('close', () => {
|
193
|
+
onAborted();
|
194
|
+
});
|
195
|
+
res.on('close', () => {
|
196
|
+
onAborted();
|
197
|
+
});
|
198
|
+
}
|
142
199
|
exports.default = router;
|
@@ -46,12 +46,13 @@ class StormClient {
|
|
46
46
|
prompt: stringPrompt,
|
47
47
|
conversationId: body.conversationId,
|
48
48
|
});
|
49
|
+
const abort = new AbortController();
|
50
|
+
options.signal = abort.signal;
|
49
51
|
const response = await fetch(options.url, options);
|
50
52
|
if (response.status !== 200) {
|
51
53
|
throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
|
52
54
|
}
|
53
55
|
const conversationId = response.headers.get(exports.ConversationIdHeader);
|
54
|
-
console.log('Received conversationId', conversationId);
|
55
56
|
const out = new stream_1.StormStream(stringPrompt, conversationId);
|
56
57
|
const jsonLStream = promises_1.default.createInterface(node_stream_1.Readable.fromWeb(response.body));
|
57
58
|
jsonLStream.on('line', (line) => {
|
@@ -63,6 +64,9 @@ class StormClient {
|
|
63
64
|
jsonLStream.on('close', () => {
|
64
65
|
out.end();
|
65
66
|
});
|
67
|
+
out.on('aborted', () => {
|
68
|
+
abort.abort();
|
69
|
+
});
|
66
70
|
return out;
|
67
71
|
}
|
68
72
|
createMetadata(prompt, conversationId) {
|
@@ -10,17 +10,22 @@ import { BlockDefinition } from '@kapeta/schemas';
|
|
10
10
|
export declare class StormStream extends EventEmitter {
|
11
11
|
private conversationId;
|
12
12
|
private lines;
|
13
|
+
private aborted;
|
13
14
|
constructor(prompt?: string, conversationId?: string | null);
|
14
15
|
getConversationId(): string;
|
16
|
+
isAborted(): boolean;
|
15
17
|
addJSONLine(line: string): void;
|
16
18
|
end(): void;
|
17
19
|
on(event: 'end', listener: () => void): this;
|
20
|
+
on(event: 'aborted', listener: () => void): this;
|
18
21
|
on(event: 'error', listener: (e: Error) => void): this;
|
19
22
|
on(event: 'data', listener: (data: StormEvent) => void): this;
|
20
23
|
emit(event: 'end'): boolean;
|
24
|
+
emit(event: 'aborted'): void;
|
21
25
|
emit(event: 'error', e: Error): boolean;
|
22
26
|
emit(event: 'data', data: StormEvent): boolean;
|
23
27
|
waitForDone(): Promise<void>;
|
28
|
+
abort(): void;
|
24
29
|
}
|
25
30
|
export interface ConversationItem {
|
26
31
|
role: 'user' | 'model';
|
@@ -9,6 +9,7 @@ const node_events_1 = require("node:events");
|
|
9
9
|
class StormStream extends node_events_1.EventEmitter {
|
10
10
|
conversationId = '';
|
11
11
|
lines = [];
|
12
|
+
aborted = false;
|
12
13
|
constructor(prompt = '', conversationId) {
|
13
14
|
super();
|
14
15
|
this.conversationId = conversationId || '';
|
@@ -16,6 +17,9 @@ class StormStream extends node_events_1.EventEmitter {
|
|
16
17
|
getConversationId() {
|
17
18
|
return this.conversationId;
|
18
19
|
}
|
20
|
+
isAborted() {
|
21
|
+
return this.aborted;
|
22
|
+
}
|
19
23
|
addJSONLine(line) {
|
20
24
|
try {
|
21
25
|
this.lines.push(line);
|
@@ -48,5 +52,12 @@ class StormStream extends node_events_1.EventEmitter {
|
|
48
52
|
});
|
49
53
|
});
|
50
54
|
}
|
55
|
+
abort() {
|
56
|
+
if (this.aborted) {
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
this.aborted = true;
|
60
|
+
this.emit('aborted');
|
61
|
+
}
|
51
62
|
}
|
52
63
|
exports.StormStream = StormStream;
|
package/package.json
CHANGED
package/src/storm/codegen.ts
CHANGED
@@ -51,6 +51,10 @@ export class StormCodegen {
|
|
51
51
|
this.out.end();
|
52
52
|
}
|
53
53
|
|
54
|
+
isAborted() {
|
55
|
+
return this.out.isAborted();
|
56
|
+
}
|
57
|
+
|
54
58
|
public getStream() {
|
55
59
|
return this.out;
|
56
60
|
}
|
@@ -110,6 +114,9 @@ export class StormCodegen {
|
|
110
114
|
* Generates the code for a block and sends it to the AI
|
111
115
|
*/
|
112
116
|
private async processBlockCode(block: BlockDefinitionInfo) {
|
117
|
+
if (this.isAborted()) {
|
118
|
+
return;
|
119
|
+
}
|
113
120
|
// Generate the code for the block using the standard codegen templates
|
114
121
|
const generatedResult = await this.generateBlock(block.content);
|
115
122
|
if (!generatedResult) {
|
@@ -121,6 +128,10 @@ export class StormCodegen {
|
|
121
128
|
// Send all the non-ai files to the stream
|
122
129
|
this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
|
123
130
|
|
131
|
+
if (this.isAborted()) {
|
132
|
+
return;
|
133
|
+
}
|
134
|
+
|
124
135
|
const relevantFiles: StormFileInfo[] = allFiles.filter(
|
125
136
|
(file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
|
126
137
|
);
|
@@ -138,9 +149,17 @@ export class StormCodegen {
|
|
138
149
|
this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
|
139
150
|
});
|
140
151
|
|
152
|
+
this.out.on('aborted', () => {
|
153
|
+
uiStream.abort();
|
154
|
+
});
|
155
|
+
|
141
156
|
await uiStream.waitForDone();
|
142
157
|
}
|
143
158
|
|
159
|
+
if (this.isAborted()) {
|
160
|
+
return;
|
161
|
+
}
|
162
|
+
|
144
163
|
// Gather the context files for implementation. These will be all be passed to the AI
|
145
164
|
const contextFiles: StormFileInfo[] = relevantFiles.filter(
|
146
165
|
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
|
@@ -160,6 +179,10 @@ export class StormCodegen {
|
|
160
179
|
|
161
180
|
const basePath = this.getBasePath(block.content.metadata.name);
|
162
181
|
|
182
|
+
if (this.isAborted()) {
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
|
163
186
|
for (const serviceFile of serviceFiles) {
|
164
187
|
const filePath = join(basePath, serviceFile.filename);
|
165
188
|
await writeFile(filePath, serviceFile.content);
|
@@ -216,6 +239,10 @@ export class StormCodegen {
|
|
216
239
|
const errorStream = await stormClient.createErrorClassification(result.error, []);
|
217
240
|
const fixes = new Map<string, Promise<string>>();
|
218
241
|
|
242
|
+
this.out.on('aborted', () => {
|
243
|
+
errorStream.abort();
|
244
|
+
});
|
245
|
+
|
219
246
|
errorStream.on('data', (evt) => {
|
220
247
|
if (evt.type === 'ERROR_CLASSIFIER') {
|
221
248
|
// find the file that caused the error
|
@@ -232,8 +259,8 @@ export class StormCodegen {
|
|
232
259
|
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
233
260
|
.map((e) => e.filename)
|
234
261
|
.join('\n')}\n---\n${content}`;
|
235
|
-
console.log(`trying to fix the code in ${eventFileName}`);
|
236
|
-
console.debug(`with the fix:\n${fix}`);
|
262
|
+
//console.log(`trying to fix the code in ${eventFileName}`);
|
263
|
+
//console.debug(`with the fix:\n${fix}`);
|
237
264
|
const code = this.codeFix(fix);
|
238
265
|
fixes.set(join(basePath, eventFileName), code);
|
239
266
|
}
|
@@ -278,6 +305,9 @@ export class StormCodegen {
|
|
278
305
|
resolve(evt.payload.content);
|
279
306
|
}
|
280
307
|
});
|
308
|
+
this.out.on('aborted', () => {
|
309
|
+
fixStream.abort();
|
310
|
+
});
|
281
311
|
fixStream.on('error', (err) => {
|
282
312
|
reject(err);
|
283
313
|
});
|
@@ -353,6 +383,10 @@ export class StormCodegen {
|
|
353
383
|
|
354
384
|
const files: StormEventFile[] = [];
|
355
385
|
|
386
|
+
this.out.on('aborted', () => {
|
387
|
+
stream.abort();
|
388
|
+
});
|
389
|
+
|
356
390
|
stream.on('data', (evt) => {
|
357
391
|
const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
|
358
392
|
if (file) {
|
@@ -411,6 +445,9 @@ export class StormCodegen {
|
|
411
445
|
* Generates the code using codegen for a given block.
|
412
446
|
*/
|
413
447
|
private async generateBlock(yamlContent: Definition) {
|
448
|
+
if (this.isAborted()) {
|
449
|
+
return;
|
450
|
+
}
|
414
451
|
if (!yamlContent.spec.target?.kind) {
|
415
452
|
//Not all block types have targets
|
416
453
|
return;
|
@@ -427,4 +464,8 @@ export class StormCodegen {
|
|
427
464
|
new CodeWriter(basePath).write(generatedResult);
|
428
465
|
return generatedResult;
|
429
466
|
}
|
467
|
+
|
468
|
+
abort() {
|
469
|
+
this.out.abort();
|
470
|
+
}
|
430
471
|
}
|
@@ -3,7 +3,15 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
|
6
|
-
import {
|
6
|
+
import {
|
7
|
+
StormBlockInfoFilled,
|
8
|
+
StormBlockType,
|
9
|
+
StormConnection,
|
10
|
+
StormEvent,
|
11
|
+
StormEventPhases,
|
12
|
+
StormEventPhaseType,
|
13
|
+
StormResourceType,
|
14
|
+
} from './events';
|
7
15
|
import {
|
8
16
|
BlockDefinition,
|
9
17
|
BlockInstance,
|
@@ -87,6 +95,24 @@ function prettifyKaplang(source: string) {
|
|
87
95
|
}
|
88
96
|
}
|
89
97
|
|
98
|
+
export function createPhaseStartEvent(type: StormEventPhaseType): StormEventPhases {
|
99
|
+
return createPhaseEvent(true, type);
|
100
|
+
}
|
101
|
+
|
102
|
+
export function createPhaseEndEvent(type: StormEventPhaseType): StormEventPhases {
|
103
|
+
return createPhaseEvent(false, type);
|
104
|
+
}
|
105
|
+
|
106
|
+
export function createPhaseEvent(start: boolean, type: StormEventPhaseType): StormEventPhases {
|
107
|
+
return {
|
108
|
+
type: start ? 'PHASE_START' : 'PHASE_END',
|
109
|
+
created: Date.now(),
|
110
|
+
payload: {
|
111
|
+
phaseType: type,
|
112
|
+
},
|
113
|
+
};
|
114
|
+
}
|
115
|
+
|
90
116
|
export async function resolveOptions(): Promise<StormOptions> {
|
91
117
|
// Predefined types for now - TODO: Allow user to select / change
|
92
118
|
|
@@ -325,7 +351,7 @@ export class StormEventParser {
|
|
325
351
|
}
|
326
352
|
|
327
353
|
public toResult(handle: string): StormDefinitions {
|
328
|
-
const planRef = StormEventParser.toRef(handle, this.planName
|
354
|
+
const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
|
329
355
|
const blockDefinitions = this.toBlockDefinitions(handle);
|
330
356
|
const refIdMap: { [key: string]: string } = {};
|
331
357
|
const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
|
@@ -337,7 +363,7 @@ export class StormEventParser {
|
|
337
363
|
block: {
|
338
364
|
ref,
|
339
365
|
},
|
340
|
-
name: block.content.metadata.title
|
366
|
+
name: block.content.metadata.title || block.content.metadata.name,
|
341
367
|
dimensions: {
|
342
368
|
left: 0,
|
343
369
|
top: 0,
|
package/src/storm/events.ts
CHANGED
@@ -220,6 +220,20 @@ export interface StormEventDefinitionChange {
|
|
220
220
|
payload: StormDefinitions;
|
221
221
|
}
|
222
222
|
|
223
|
+
export enum StormEventPhaseType {
|
224
|
+
META = 'META',
|
225
|
+
DEFINITIONS = 'DEFINITIONS',
|
226
|
+
IMPLEMENTATION = 'IMPLEMENTATION',
|
227
|
+
}
|
228
|
+
|
229
|
+
export interface StormEventPhases {
|
230
|
+
type: 'PHASE_START' | 'PHASE_END';
|
231
|
+
created: number;
|
232
|
+
payload: {
|
233
|
+
phaseType: StormEventPhaseType;
|
234
|
+
};
|
235
|
+
}
|
236
|
+
|
223
237
|
export type StormEvent =
|
224
238
|
| StormEventCreateBlock
|
225
239
|
| StormEventCreateConnection
|
@@ -236,4 +250,5 @@ export type StormEvent =
|
|
236
250
|
| StormEventDefinitionChange
|
237
251
|
| StormEventErrorClassifier
|
238
252
|
| StormEventCodeFix
|
239
|
-
| StormEventBlockReady
|
253
|
+
| StormEventBlockReady
|
254
|
+
| StormEventPhases;
|
package/src/storm/routes.ts
CHANGED
@@ -11,12 +11,17 @@ import { stringBody } from '../middleware/stringBody';
|
|
11
11
|
import { KapetaBodyRequest } from '../types';
|
12
12
|
import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
|
13
13
|
import { ConversationIdHeader, stormClient } from './stormClient';
|
14
|
-
import { StormEvent } from './events';
|
15
|
-
import {
|
14
|
+
import { StormEvent, StormEventPhaseType } from './events';
|
15
|
+
import {
|
16
|
+
createPhaseEndEvent,
|
17
|
+
createPhaseStartEvent,
|
18
|
+
resolveOptions,
|
19
|
+
StormDefinitions,
|
20
|
+
StormEventParser,
|
21
|
+
} from './event-parser';
|
16
22
|
import { StormCodegen } from './codegen';
|
17
23
|
import { assetManager } from '../assetManager';
|
18
24
|
import Path from 'path';
|
19
|
-
import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
|
20
25
|
|
21
26
|
const router = Router();
|
22
27
|
|
@@ -36,18 +41,48 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
36
41
|
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
37
42
|
const metaStream = await stormClient.createMetadata(aiRequest.prompt, conversationId);
|
38
43
|
|
44
|
+
onRequestAborted(req, res, () => {
|
45
|
+
metaStream.abort();
|
46
|
+
});
|
47
|
+
|
39
48
|
res.set('Content-Type', 'application/x-ndjson');
|
40
49
|
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
41
50
|
res.set(ConversationIdHeader, metaStream.getConversationId());
|
42
51
|
|
52
|
+
let currentPhase = StormEventPhaseType.META;
|
53
|
+
|
43
54
|
metaStream.on('data', (data: StormEvent) => {
|
44
55
|
const result = eventParser.processEvent(req.params.handle, data);
|
45
56
|
|
57
|
+
switch (data.type) {
|
58
|
+
case 'CREATE_API':
|
59
|
+
case 'CREATE_MODEL':
|
60
|
+
case 'CREATE_TYPE':
|
61
|
+
if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
|
62
|
+
sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
|
63
|
+
currentPhase = StormEventPhaseType.DEFINITIONS;
|
64
|
+
sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
|
65
|
+
}
|
66
|
+
break;
|
67
|
+
}
|
68
|
+
|
46
69
|
sendEvent(res, data);
|
47
70
|
sendDefinitions(res, result);
|
48
71
|
});
|
49
72
|
|
50
|
-
|
73
|
+
try {
|
74
|
+
sendEvent(res, createPhaseStartEvent(StormEventPhaseType.META));
|
75
|
+
|
76
|
+
await waitForStormStream(metaStream);
|
77
|
+
} finally {
|
78
|
+
if (!metaStream.isAborted()) {
|
79
|
+
sendEvent(res, createPhaseEndEvent(currentPhase));
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
if (metaStream.isAborted()) {
|
84
|
+
return;
|
85
|
+
}
|
51
86
|
|
52
87
|
if (!eventParser.isValid()) {
|
53
88
|
// We can't continue if the meta stream is invalid
|
@@ -63,27 +98,44 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
63
98
|
|
64
99
|
const result = eventParser.toResult(handle);
|
65
100
|
|
101
|
+
if (metaStream.isAborted()) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
66
105
|
sendDefinitions(res, result);
|
67
106
|
|
68
107
|
if (!req.query.skipCodegen) {
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
108
|
+
try {
|
109
|
+
sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENTATION));
|
110
|
+
const stormCodegen = new StormCodegen(
|
111
|
+
metaStream.getConversationId(),
|
112
|
+
aiRequest.prompt,
|
113
|
+
result.blocks,
|
114
|
+
eventParser.getEvents()
|
115
|
+
);
|
116
|
+
|
117
|
+
onRequestAborted(req, res, () => {
|
118
|
+
stormCodegen.abort();
|
119
|
+
});
|
120
|
+
|
121
|
+
const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
|
122
|
+
|
123
|
+
await stormCodegen.process();
|
124
|
+
|
125
|
+
await codegenPromise;
|
126
|
+
} finally {
|
127
|
+
if (!metaStream.isAborted()) {
|
128
|
+
sendEvent(res, createPhaseEndEvent(StormEventPhaseType.IMPLEMENTATION));
|
129
|
+
}
|
130
|
+
}
|
81
131
|
}
|
82
132
|
|
83
133
|
sendDone(res);
|
84
134
|
} catch (err: any) {
|
85
135
|
sendError(err, res);
|
86
|
-
res.
|
136
|
+
if (!res.closed) {
|
137
|
+
res.end();
|
138
|
+
}
|
87
139
|
}
|
88
140
|
});
|
89
141
|
|
@@ -93,19 +145,13 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
|
|
93
145
|
try {
|
94
146
|
const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
|
95
147
|
|
96
|
-
console.log('Creating block at', ymlPath);
|
97
|
-
|
98
148
|
const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
|
99
149
|
|
100
150
|
if (await FS.pathExists(createRequest.tmpPath)) {
|
101
|
-
console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
|
102
|
-
|
103
151
|
await FS.move(createRequest.tmpPath, createRequest.newPath, {
|
104
152
|
overwrite: true,
|
105
153
|
});
|
106
154
|
|
107
|
-
console.log('Updating asset', asset.ref);
|
108
|
-
|
109
155
|
res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
|
110
156
|
} else {
|
111
157
|
res.send(asset);
|
@@ -125,6 +171,9 @@ function sendDefinitions(res: Response, result: StormDefinitions) {
|
|
125
171
|
}
|
126
172
|
|
127
173
|
function sendDone(res: Response) {
|
174
|
+
if (res.closed) {
|
175
|
+
return;
|
176
|
+
}
|
128
177
|
sendEvent(res, {
|
129
178
|
type: 'DONE',
|
130
179
|
created: Date.now(),
|
@@ -134,6 +183,9 @@ function sendDone(res: Response) {
|
|
134
183
|
}
|
135
184
|
|
136
185
|
function sendError(err: Error, res: Response) {
|
186
|
+
if (res.closed) {
|
187
|
+
return;
|
188
|
+
}
|
137
189
|
console.error('Failed to send prompt', err);
|
138
190
|
if (res.headersSent) {
|
139
191
|
sendEvent(res, {
|
@@ -175,7 +227,19 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
|
|
175
227
|
}
|
176
228
|
|
177
229
|
function sendEvent(res: Response, evt: StormEvent) {
|
230
|
+
if (res.closed) {
|
231
|
+
return;
|
232
|
+
}
|
178
233
|
res.write(JSON.stringify(evt) + '\n');
|
179
234
|
}
|
180
235
|
|
236
|
+
function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () => void) {
|
237
|
+
req.on('close', () => {
|
238
|
+
onAborted();
|
239
|
+
});
|
240
|
+
res.on('close', () => {
|
241
|
+
onAborted();
|
242
|
+
});
|
243
|
+
}
|
244
|
+
|
181
245
|
export default router;
|