@kapeta/local-cluster-service 0.48.5 → 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 +14 -0
- package/dist/cjs/src/assetManager.d.ts +1 -1
- package/dist/cjs/src/assetManager.js +5 -3
- package/dist/cjs/src/filesystem/routes.js +10 -0
- package/dist/cjs/src/storm/codegen.d.ts +4 -1
- package/dist/cjs/src/storm/codegen.js +63 -11
- package/dist/cjs/src/storm/event-parser.d.ts +5 -2
- package/dist/cjs/src/storm/event-parser.js +24 -4
- package/dist/cjs/src/storm/events.d.ts +24 -1
- package/dist/cjs/src/storm/events.js +7 -0
- package/dist/cjs/src/storm/routes.js +88 -6
- package/dist/cjs/src/storm/stormClient.js +5 -1
- package/dist/cjs/src/storm/stream.d.ts +11 -0
- package/dist/cjs/src/storm/stream.js +11 -0
- package/dist/esm/src/assetManager.d.ts +1 -1
- package/dist/esm/src/assetManager.js +5 -3
- package/dist/esm/src/filesystem/routes.js +10 -0
- package/dist/esm/src/storm/codegen.d.ts +4 -1
- package/dist/esm/src/storm/codegen.js +63 -11
- package/dist/esm/src/storm/event-parser.d.ts +5 -2
- package/dist/esm/src/storm/event-parser.js +24 -4
- package/dist/esm/src/storm/events.d.ts +24 -1
- package/dist/esm/src/storm/events.js +7 -0
- package/dist/esm/src/storm/routes.js +88 -6
- package/dist/esm/src/storm/stormClient.js +5 -1
- package/dist/esm/src/storm/stream.d.ts +11 -0
- package/dist/esm/src/storm/stream.js +11 -0
- package/package.json +1 -1
- package/src/assetManager.ts +6 -4
- package/src/filesystem/routes.ts +10 -0
- package/src/storm/codegen.ts +95 -22
- package/src/storm/event-parser.ts +33 -5
- package/src/storm/events.ts +32 -4
- package/src/storm/routes.ts +113 -12
- package/src/storm/stormClient.ts +8 -1
- package/src/storm/stream.ts +22 -0
package/src/filesystem/routes.ts
CHANGED
@@ -8,6 +8,7 @@ import { stringBody, StringBodyRequest } from '../middleware/stringBody';
|
|
8
8
|
import { filesystemManager } from '../filesystemManager';
|
9
9
|
import { corsHandler } from '../middleware/cors';
|
10
10
|
import { NextFunction, Request, Response } from 'express';
|
11
|
+
import FS from 'fs-extra';
|
11
12
|
|
12
13
|
let router = Router();
|
13
14
|
|
@@ -99,6 +100,15 @@ router.get('/readfile', async (req: Request, res: Response) => {
|
|
99
100
|
}
|
100
101
|
});
|
101
102
|
|
103
|
+
router.get('/exists', async (req: Request, res: Response) => {
|
104
|
+
let pathArg = req.query.path as string;
|
105
|
+
try {
|
106
|
+
res.send(await FS.pathExists(pathArg));
|
107
|
+
} catch (err) {
|
108
|
+
res.status(400).send({ error: '' + err });
|
109
|
+
}
|
110
|
+
});
|
111
|
+
|
102
112
|
router.put('/mkdir', async (req: Request, res: Response) => {
|
103
113
|
let pathArg = req.query.path as string;
|
104
114
|
try {
|
package/src/storm/codegen.ts
CHANGED
@@ -4,18 +4,26 @@
|
|
4
4
|
*/
|
5
5
|
|
6
6
|
import { Definition } from '@kapeta/local-cluster-config';
|
7
|
-
import {
|
7
|
+
import {
|
8
|
+
AIFileTypes,
|
9
|
+
BlockCodeGenerator,
|
10
|
+
CodeGenerator,
|
11
|
+
CodeWriter,
|
12
|
+
GeneratedFile,
|
13
|
+
GeneratedResult,
|
14
|
+
} from '@kapeta/codegen';
|
8
15
|
import { BlockDefinition } from '@kapeta/schemas';
|
9
16
|
import { codeGeneratorManager } from '../codeGeneratorManager';
|
10
17
|
import { STORM_ID, stormClient } from './stormClient';
|
11
18
|
import { StormEvent, StormEventFile } from './events';
|
12
19
|
import { BlockDefinitionInfo, StormEventParser } from './event-parser';
|
13
|
-
import {
|
14
|
-
import { KapetaURI } from '@kapeta/nodejs-utils';
|
20
|
+
import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
|
21
|
+
import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
|
15
22
|
import { writeFile } from 'fs/promises';
|
16
23
|
import path, { join } from 'path';
|
17
24
|
import os from 'node:os';
|
18
25
|
import { readFile, readFileSync, writeFileSync } from 'fs';
|
26
|
+
import Path from 'path';
|
19
27
|
|
20
28
|
type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
|
21
29
|
|
@@ -25,22 +33,28 @@ export class StormCodegen {
|
|
25
33
|
private readonly out = new StormStream();
|
26
34
|
private readonly events: StormEvent[];
|
27
35
|
private readonly tmpDir: string;
|
36
|
+
private readonly conversationId: string;
|
28
37
|
|
29
|
-
constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
38
|
+
constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
30
39
|
this.userPrompt = userPrompt;
|
31
40
|
this.blocks = blocks;
|
32
41
|
this.events = events;
|
33
|
-
this.tmpDir = os.tmpdir();
|
42
|
+
this.tmpDir = Path.join(os.tmpdir(), conversationId);
|
43
|
+
this.conversationId = conversationId;
|
34
44
|
}
|
35
45
|
|
36
46
|
public async process() {
|
37
|
-
|
38
|
-
|
39
|
-
}
|
40
|
-
|
47
|
+
const promises = this.blocks.map((block) => {
|
48
|
+
return this.processBlockCode(block);
|
49
|
+
});
|
50
|
+
await Promise.all(promises);
|
41
51
|
this.out.end();
|
42
52
|
}
|
43
53
|
|
54
|
+
isAborted() {
|
55
|
+
return this.out.isAborted();
|
56
|
+
}
|
57
|
+
|
44
58
|
public getStream() {
|
45
59
|
return this.out;
|
46
60
|
}
|
@@ -100,6 +114,9 @@ export class StormCodegen {
|
|
100
114
|
* Generates the code for a block and sends it to the AI
|
101
115
|
*/
|
102
116
|
private async processBlockCode(block: BlockDefinitionInfo) {
|
117
|
+
if (this.isAborted()) {
|
118
|
+
return;
|
119
|
+
}
|
103
120
|
// Generate the code for the block using the standard codegen templates
|
104
121
|
const generatedResult = await this.generateBlock(block.content);
|
105
122
|
if (!generatedResult) {
|
@@ -109,7 +126,11 @@ export class StormCodegen {
|
|
109
126
|
const allFiles = this.toStormFiles(generatedResult);
|
110
127
|
|
111
128
|
// Send all the non-ai files to the stream
|
112
|
-
this.emitFiles(block.uri, block.aiName, allFiles);
|
129
|
+
this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
|
130
|
+
|
131
|
+
if (this.isAborted()) {
|
132
|
+
return;
|
133
|
+
}
|
113
134
|
|
114
135
|
const relevantFiles: StormFileInfo[] = allFiles.filter(
|
115
136
|
(file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
|
@@ -125,12 +146,20 @@ export class StormCodegen {
|
|
125
146
|
});
|
126
147
|
|
127
148
|
uiStream.on('data', (evt) => {
|
128
|
-
this.handleUiOutput(block.uri, block.aiName, evt);
|
149
|
+
this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
|
150
|
+
});
|
151
|
+
|
152
|
+
this.out.on('aborted', () => {
|
153
|
+
uiStream.abort();
|
129
154
|
});
|
130
155
|
|
131
156
|
await uiStream.waitForDone();
|
132
157
|
}
|
133
158
|
|
159
|
+
if (this.isAborted()) {
|
160
|
+
return;
|
161
|
+
}
|
162
|
+
|
134
163
|
// Gather the context files for implementation. These will be all be passed to the AI
|
135
164
|
const contextFiles: StormFileInfo[] = relevantFiles.filter(
|
136
165
|
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
|
@@ -140,7 +169,7 @@ export class StormCodegen {
|
|
140
169
|
const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
|
141
170
|
if (serviceFiles.length > 0) {
|
142
171
|
await this.processTemplates(
|
143
|
-
block.uri,
|
172
|
+
parseKapetaUri(block.uri),
|
144
173
|
block.aiName,
|
145
174
|
stormClient.createServiceImplementation.bind(stormClient),
|
146
175
|
serviceFiles,
|
@@ -150,6 +179,10 @@ export class StormCodegen {
|
|
150
179
|
|
151
180
|
const basePath = this.getBasePath(block.content.metadata.name);
|
152
181
|
|
182
|
+
if (this.isAborted()) {
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
|
153
186
|
for (const serviceFile of serviceFiles) {
|
154
187
|
const filePath = join(basePath, serviceFile.filename);
|
155
188
|
await writeFile(filePath, serviceFile.content);
|
@@ -168,18 +201,36 @@ export class StormCodegen {
|
|
168
201
|
const filesToBeFixed = serviceFiles.concat(contextFiles);
|
169
202
|
const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
|
170
203
|
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
204
|
+
|
205
|
+
const blockRef = block.uri;
|
206
|
+
this.out.emit('data', {
|
207
|
+
type: 'BLOCK_READY',
|
208
|
+
reason: 'Block ready',
|
209
|
+
created: Date.now(),
|
210
|
+
payload: {
|
211
|
+
path: basePath,
|
212
|
+
blockName: block.aiName,
|
213
|
+
blockRef,
|
214
|
+
instanceId: StormEventParser.toInstanceIdFromRef(blockRef),
|
215
|
+
},
|
216
|
+
} satisfies StormEvent);
|
171
217
|
}
|
172
218
|
|
173
|
-
private async verifyAndFixCode(
|
219
|
+
private async verifyAndFixCode(
|
220
|
+
codeGenerator: CodeGenerator,
|
221
|
+
basePath: string,
|
222
|
+
filesToBeFixed: StormFileInfo[],
|
223
|
+
knownFiles: StormFileInfo[]
|
224
|
+
) {
|
174
225
|
let attempts = 0;
|
175
226
|
let validCode = false;
|
176
227
|
for (let i = 0; i <= 3; i++) {
|
177
228
|
attempts++;
|
178
229
|
try {
|
179
|
-
console.log(`Validating the code in ${basePath} attempt #${attempts}`)
|
230
|
+
console.log(`Validating the code in ${basePath} attempt #${attempts}`);
|
180
231
|
const result = await codeGenerator.validateForTarget(basePath);
|
181
232
|
if (result && result.valid) {
|
182
|
-
|
233
|
+
validCode = true;
|
183
234
|
break;
|
184
235
|
}
|
185
236
|
|
@@ -188,20 +239,28 @@ export class StormCodegen {
|
|
188
239
|
const errorStream = await stormClient.createErrorClassification(result.error, []);
|
189
240
|
const fixes = new Map<string, Promise<string>>();
|
190
241
|
|
242
|
+
this.out.on('aborted', () => {
|
243
|
+
errorStream.abort();
|
244
|
+
});
|
245
|
+
|
191
246
|
errorStream.on('data', (evt) => {
|
192
247
|
if (evt.type === 'ERROR_CLASSIFIER') {
|
193
248
|
// find the file that caused the error
|
194
249
|
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
195
|
-
const eventFileName = this.removePrefix(basePath+'/', evt.payload.filename);
|
250
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
196
251
|
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
197
|
-
if(!file) {
|
198
|
-
console.log(
|
252
|
+
if (!file) {
|
253
|
+
console.log(
|
254
|
+
`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
|
255
|
+
);
|
199
256
|
}
|
200
257
|
// read the content of the file
|
201
258
|
const content = readFileSync(join(basePath, eventFileName), 'utf8');
|
202
|
-
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
203
|
-
|
204
|
-
|
259
|
+
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
260
|
+
.map((e) => e.filename)
|
261
|
+
.join('\n')}\n---\n${content}`;
|
262
|
+
//console.log(`trying to fix the code in ${eventFileName}`);
|
263
|
+
//console.debug(`with the fix:\n${fix}`);
|
205
264
|
const code = this.codeFix(fix);
|
206
265
|
fixes.set(join(basePath, eventFileName), code);
|
207
266
|
}
|
@@ -226,7 +285,7 @@ export class StormCodegen {
|
|
226
285
|
|
227
286
|
removePrefix(prefix: string, str: string): string {
|
228
287
|
if (str.startsWith(prefix)) {
|
229
|
-
|
288
|
+
return str.slice(prefix.length);
|
230
289
|
}
|
231
290
|
return str;
|
232
291
|
}
|
@@ -246,6 +305,9 @@ export class StormCodegen {
|
|
246
305
|
resolve(evt.payload.content);
|
247
306
|
}
|
248
307
|
});
|
308
|
+
this.out.on('aborted', () => {
|
309
|
+
fixStream.abort();
|
310
|
+
});
|
249
311
|
fixStream.on('error', (err) => {
|
250
312
|
reject(err);
|
251
313
|
});
|
@@ -321,6 +383,10 @@ export class StormCodegen {
|
|
321
383
|
|
322
384
|
const files: StormEventFile[] = [];
|
323
385
|
|
386
|
+
this.out.on('aborted', () => {
|
387
|
+
stream.abort();
|
388
|
+
});
|
389
|
+
|
324
390
|
stream.on('data', (evt) => {
|
325
391
|
const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
|
326
392
|
if (file) {
|
@@ -379,6 +445,9 @@ export class StormCodegen {
|
|
379
445
|
* Generates the code using codegen for a given block.
|
380
446
|
*/
|
381
447
|
private async generateBlock(yamlContent: Definition) {
|
448
|
+
if (this.isAborted()) {
|
449
|
+
return;
|
450
|
+
}
|
382
451
|
if (!yamlContent.spec.target?.kind) {
|
383
452
|
//Not all block types have targets
|
384
453
|
return;
|
@@ -395,4 +464,8 @@ export class StormCodegen {
|
|
395
464
|
new CodeWriter(basePath).write(generatedResult);
|
396
465
|
return generatedResult;
|
397
466
|
}
|
467
|
+
|
468
|
+
abort() {
|
469
|
+
this.out.abort();
|
470
|
+
}
|
398
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,
|
@@ -29,7 +37,7 @@ import { v5 as uuid } from 'uuid';
|
|
29
37
|
import { definitionsManager } from '../definitionsManager';
|
30
38
|
|
31
39
|
export interface BlockDefinitionInfo {
|
32
|
-
uri:
|
40
|
+
uri: string;
|
33
41
|
content: BlockDefinition;
|
34
42
|
aiName: string;
|
35
43
|
}
|
@@ -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,
|
@@ -454,6 +480,8 @@ export class StormEventParser {
|
|
454
480
|
name: planRef.fullName,
|
455
481
|
title: this.planName,
|
456
482
|
description: this.planDescription,
|
483
|
+
structure: 'mono',
|
484
|
+
visibility: 'private',
|
457
485
|
},
|
458
486
|
spec: {
|
459
487
|
blocks,
|
@@ -474,7 +502,7 @@ export class StormEventParser {
|
|
474
502
|
const blockRef = StormEventParser.toRef(handle, blockInfo.name);
|
475
503
|
|
476
504
|
const blockDefinitionInfo: BlockDefinitionInfo = {
|
477
|
-
uri: blockRef,
|
505
|
+
uri: blockRef.toNormalizedString(),
|
478
506
|
aiName: blockInfo.name,
|
479
507
|
content: {
|
480
508
|
kind: this.toBlockKind(blockInfo.type),
|
package/src/storm/events.ts
CHANGED
@@ -144,9 +144,9 @@ export interface StormEventCodeFix {
|
|
144
144
|
};
|
145
145
|
}
|
146
146
|
export interface StormEventErrorClassifierInfo {
|
147
|
-
error: string
|
148
|
-
filename: string
|
149
|
-
potentialFix: string
|
147
|
+
error: string;
|
148
|
+
filename: string;
|
149
|
+
potentialFix: string;
|
150
150
|
}
|
151
151
|
|
152
152
|
export interface ScreenTemplate {
|
@@ -196,6 +196,18 @@ export interface StormEventFile {
|
|
196
196
|
};
|
197
197
|
}
|
198
198
|
|
199
|
+
export interface StormEventBlockReady {
|
200
|
+
type: 'BLOCK_READY';
|
201
|
+
reason: string;
|
202
|
+
created: number;
|
203
|
+
payload: {
|
204
|
+
path: string;
|
205
|
+
blockName: string;
|
206
|
+
blockRef: string;
|
207
|
+
instanceId: string;
|
208
|
+
};
|
209
|
+
}
|
210
|
+
|
199
211
|
export interface StormEventDone {
|
200
212
|
type: 'DONE';
|
201
213
|
created: number;
|
@@ -208,6 +220,20 @@ export interface StormEventDefinitionChange {
|
|
208
220
|
payload: StormDefinitions;
|
209
221
|
}
|
210
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
|
+
|
211
237
|
export type StormEvent =
|
212
238
|
| StormEventCreateBlock
|
213
239
|
| StormEventCreateConnection
|
@@ -223,4 +249,6 @@ export type StormEvent =
|
|
223
249
|
| StormEventDone
|
224
250
|
| StormEventDefinitionChange
|
225
251
|
| StormEventErrorClassifier
|
226
|
-
| StormEventCodeFix
|
252
|
+
| StormEventCodeFix
|
253
|
+
| StormEventBlockReady
|
254
|
+
| StormEventPhases;
|
package/src/storm/routes.ts
CHANGED
@@ -4,15 +4,24 @@
|
|
4
4
|
*/
|
5
5
|
|
6
6
|
import Router from 'express-promise-router';
|
7
|
+
import FS from 'fs-extra';
|
7
8
|
import { Response } from 'express';
|
8
9
|
import { corsHandler } from '../middleware/cors';
|
9
10
|
import { stringBody } from '../middleware/stringBody';
|
10
11
|
import { KapetaBodyRequest } from '../types';
|
11
|
-
import { StormContextRequest,
|
12
|
+
import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
|
12
13
|
import { ConversationIdHeader, stormClient } from './stormClient';
|
13
|
-
import { StormEvent } from './events';
|
14
|
-
import {
|
14
|
+
import { StormEvent, StormEventPhaseType } from './events';
|
15
|
+
import {
|
16
|
+
createPhaseEndEvent,
|
17
|
+
createPhaseStartEvent,
|
18
|
+
resolveOptions,
|
19
|
+
StormDefinitions,
|
20
|
+
StormEventParser,
|
21
|
+
} from './event-parser';
|
15
22
|
import { StormCodegen } from './codegen';
|
23
|
+
import { assetManager } from '../assetManager';
|
24
|
+
import Path from 'path';
|
16
25
|
|
17
26
|
const router = Router();
|
18
27
|
|
@@ -32,18 +41,48 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
32
41
|
const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
|
33
42
|
const metaStream = await stormClient.createMetadata(aiRequest.prompt, conversationId);
|
34
43
|
|
44
|
+
onRequestAborted(req, res, () => {
|
45
|
+
metaStream.abort();
|
46
|
+
});
|
47
|
+
|
35
48
|
res.set('Content-Type', 'application/x-ndjson');
|
36
49
|
res.set('Access-Control-Expose-Headers', ConversationIdHeader);
|
37
50
|
res.set(ConversationIdHeader, metaStream.getConversationId());
|
38
51
|
|
52
|
+
let currentPhase = StormEventPhaseType.META;
|
53
|
+
|
39
54
|
metaStream.on('data', (data: StormEvent) => {
|
40
55
|
const result = eventParser.processEvent(req.params.handle, data);
|
41
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
|
+
|
42
69
|
sendEvent(res, data);
|
43
70
|
sendDefinitions(res, result);
|
44
71
|
});
|
45
72
|
|
46
|
-
|
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
|
+
}
|
47
86
|
|
48
87
|
if (!eventParser.isValid()) {
|
49
88
|
// We can't continue if the meta stream is invalid
|
@@ -59,22 +98,66 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
|
59
98
|
|
60
99
|
const result = eventParser.toResult(handle);
|
61
100
|
|
101
|
+
if (metaStream.isAborted()) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
62
105
|
sendDefinitions(res, result);
|
63
106
|
|
64
107
|
if (!req.query.skipCodegen) {
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
+
}
|
72
131
|
}
|
73
132
|
|
74
133
|
sendDone(res);
|
75
134
|
} catch (err: any) {
|
76
135
|
sendError(err, res);
|
77
|
-
res.
|
136
|
+
if (!res.closed) {
|
137
|
+
res.end();
|
138
|
+
}
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
|
143
|
+
const createRequest: StormCreateBlockRequest = JSON.parse(req.stringBody ?? '{}');
|
144
|
+
|
145
|
+
try {
|
146
|
+
const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
|
147
|
+
|
148
|
+
const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
|
149
|
+
|
150
|
+
if (await FS.pathExists(createRequest.tmpPath)) {
|
151
|
+
await FS.move(createRequest.tmpPath, createRequest.newPath, {
|
152
|
+
overwrite: true,
|
153
|
+
});
|
154
|
+
|
155
|
+
res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
|
156
|
+
} else {
|
157
|
+
res.send(asset);
|
158
|
+
}
|
159
|
+
} catch (err: any) {
|
160
|
+
res.status(500).send({ error: err.message });
|
78
161
|
}
|
79
162
|
});
|
80
163
|
|
@@ -88,6 +171,9 @@ function sendDefinitions(res: Response, result: StormDefinitions) {
|
|
88
171
|
}
|
89
172
|
|
90
173
|
function sendDone(res: Response) {
|
174
|
+
if (res.closed) {
|
175
|
+
return;
|
176
|
+
}
|
91
177
|
sendEvent(res, {
|
92
178
|
type: 'DONE',
|
93
179
|
created: Date.now(),
|
@@ -97,6 +183,9 @@ function sendDone(res: Response) {
|
|
97
183
|
}
|
98
184
|
|
99
185
|
function sendError(err: Error, res: Response) {
|
186
|
+
if (res.closed) {
|
187
|
+
return;
|
188
|
+
}
|
100
189
|
console.error('Failed to send prompt', err);
|
101
190
|
if (res.headersSent) {
|
102
191
|
sendEvent(res, {
|
@@ -138,7 +227,19 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
|
|
138
227
|
}
|
139
228
|
|
140
229
|
function sendEvent(res: Response, evt: StormEvent) {
|
230
|
+
if (res.closed) {
|
231
|
+
return;
|
232
|
+
}
|
141
233
|
res.write(JSON.stringify(evt) + '\n');
|
142
234
|
}
|
143
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
|
+
|
144
245
|
export default router;
|
package/src/storm/stormClient.ts
CHANGED
@@ -13,6 +13,7 @@ import {
|
|
13
13
|
StormStream,
|
14
14
|
StormUIImplementationPrompt,
|
15
15
|
} from './stream';
|
16
|
+
import { getRawAsset } from 'node:sea';
|
16
17
|
|
17
18
|
export const STORM_ID = 'storm';
|
18
19
|
|
@@ -60,6 +61,9 @@ class StormClient {
|
|
60
61
|
conversationId: body.conversationId,
|
61
62
|
});
|
62
63
|
|
64
|
+
const abort = new AbortController();
|
65
|
+
options.signal = abort.signal;
|
66
|
+
|
63
67
|
const response = await fetch(options.url, options);
|
64
68
|
|
65
69
|
if (response.status !== 200) {
|
@@ -69,7 +73,6 @@ class StormClient {
|
|
69
73
|
}
|
70
74
|
|
71
75
|
const conversationId = response.headers.get(ConversationIdHeader);
|
72
|
-
console.log('Received conversationId', conversationId);
|
73
76
|
|
74
77
|
const out = new StormStream(stringPrompt, conversationId);
|
75
78
|
|
@@ -87,6 +90,10 @@ class StormClient {
|
|
87
90
|
out.end();
|
88
91
|
});
|
89
92
|
|
93
|
+
out.on('aborted', () => {
|
94
|
+
abort.abort();
|
95
|
+
});
|
96
|
+
|
90
97
|
return out;
|
91
98
|
}
|
92
99
|
|