@kapeta/local-cluster-service 0.51.4 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cjs/src/storm/codegen.d.ts +5 -0
- package/dist/cjs/src/storm/codegen.js +146 -47
- package/dist/cjs/src/storm/events.d.ts +18 -4
- package/dist/cjs/src/storm/stormClient.d.ts +1 -0
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/esm/src/storm/codegen.d.ts +5 -0
- package/dist/esm/src/storm/codegen.js +146 -47
- package/dist/esm/src/storm/events.d.ts +18 -4
- package/dist/esm/src/storm/stormClient.d.ts +1 -0
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/package.json +2 -2
- package/src/storm/codegen.ts +187 -53
- package/src/storm/events.ts +21 -3
- package/src/storm/stormClient.ts +8 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## [0.52.1](https://github.com/kapetacom/local-cluster-service/compare/v0.52.0...v0.52.1) (2024-06-12)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* adjust errorclassifier, add errordetails and adjust codefix ([1eb8000](https://github.com/kapetacom/local-cluster-service/commit/1eb80005f59c103d558219d07bed6f01b9ebc562))
|
7
|
+
* look for similar files when file asked for does not exist ([abe75c4](https://github.com/kapetacom/local-cluster-service/commit/abe75c4fbcbd812565aa154d6437a288e58403ac))
|
8
|
+
* review comments ([542f3dc](https://github.com/kapetacom/local-cluster-service/commit/542f3dca02fc006ee0bd7adf2ad76f28295ba6f2))
|
9
|
+
|
10
|
+
# [0.52.0](https://github.com/kapetacom/local-cluster-service/compare/v0.51.4...v0.52.0) (2024-06-10)
|
11
|
+
|
12
|
+
|
13
|
+
### Features
|
14
|
+
|
15
|
+
* bump codegen to get codeGenerator.validateForTarget ([#171](https://github.com/kapetacom/local-cluster-service/issues/171)) ([2969f15](https://github.com/kapetacom/local-cluster-service/commit/2969f15a4c7909af58886310aa04140b8b890751))
|
16
|
+
|
1
17
|
## [0.51.4](https://github.com/kapetacom/local-cluster-service/compare/v0.51.3...v0.51.4) (2024-06-10)
|
2
18
|
|
3
19
|
|
@@ -26,6 +26,11 @@ export declare class StormCodegen {
|
|
26
26
|
*/
|
27
27
|
private processBlockCode;
|
28
28
|
private verifyAndFixCode;
|
29
|
+
private tryToFixFile;
|
30
|
+
private classifyErrors;
|
31
|
+
private getErrorDetailsForFile;
|
32
|
+
private createFixRequestForFile;
|
33
|
+
private getErrorLine;
|
29
34
|
removePrefix(prefix: string, str: string): string;
|
30
35
|
/**
|
31
36
|
* Sends the code to the AI for a fix
|
@@ -38,11 +38,13 @@ const event_parser_1 = require("./event-parser");
|
|
38
38
|
const stream_1 = require("./stream");
|
39
39
|
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
40
40
|
const promises_1 = require("fs/promises");
|
41
|
-
const path_1 =
|
41
|
+
const path_1 = __importDefault(require("path"));
|
42
|
+
const path_2 = __importStar(require("path"));
|
42
43
|
const node_os_1 = __importDefault(require("node:os"));
|
43
44
|
const fs_1 = require("fs");
|
44
|
-
const path_2 = __importDefault(require("path"));
|
45
45
|
const yaml_1 = __importDefault(require("yaml"));
|
46
|
+
const fs = __importStar(require("node:fs"));
|
47
|
+
const uuid_1 = require("uuid");
|
46
48
|
const SIMULATED_DELAY = 1000;
|
47
49
|
class SimulatedFileDelay {
|
48
50
|
file;
|
@@ -197,7 +199,7 @@ class StormCodegen {
|
|
197
199
|
...data,
|
198
200
|
payload: {
|
199
201
|
...data.payload,
|
200
|
-
path: (0,
|
202
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
201
203
|
blockName,
|
202
204
|
blockRef,
|
203
205
|
instanceId,
|
@@ -209,7 +211,7 @@ class StormCodegen {
|
|
209
211
|
...data,
|
210
212
|
payload: {
|
211
213
|
...data.payload,
|
212
|
-
path: (0,
|
214
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
213
215
|
blockName,
|
214
216
|
blockRef,
|
215
217
|
instanceId,
|
@@ -221,7 +223,7 @@ class StormCodegen {
|
|
221
223
|
...data,
|
222
224
|
payload: {
|
223
225
|
...data.payload,
|
224
|
-
path: (0,
|
226
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
225
227
|
blockName,
|
226
228
|
blockRef,
|
227
229
|
instanceId,
|
@@ -241,7 +243,7 @@ class StormCodegen {
|
|
241
243
|
created: Date.now(),
|
242
244
|
payload: {
|
243
245
|
filename: data.payload.filename,
|
244
|
-
path: (0,
|
246
|
+
path: (0, path_2.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
|
245
247
|
content: data.payload.content,
|
246
248
|
blockRef: ref,
|
247
249
|
instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
|
@@ -297,7 +299,7 @@ class StormCodegen {
|
|
297
299
|
}
|
298
300
|
// Gather the context files for implementation. These will be all be passed to the AI
|
299
301
|
const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
|
300
|
-
// Send the service and UI templates to the AI. These will be
|
302
|
+
// Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
|
301
303
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
302
304
|
if (serviceFiles.length > 0) {
|
303
305
|
await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
@@ -307,17 +309,17 @@ class StormCodegen {
|
|
307
309
|
return;
|
308
310
|
}
|
309
311
|
for (const serviceFile of serviceFiles) {
|
310
|
-
const filePath = (0,
|
312
|
+
const filePath = (0, path_2.join)(basePath, serviceFile.filename);
|
311
313
|
await (0, promises_1.writeFile)(filePath, serviceFile.content);
|
312
314
|
}
|
313
315
|
for (const serviceFile of contextFiles) {
|
314
|
-
const filePath = (0,
|
316
|
+
const filePath = (0, path_2.join)(basePath, serviceFile.filename);
|
315
317
|
await (0, promises_1.writeFile)(filePath, serviceFile.content);
|
316
318
|
}
|
317
|
-
const kapetaYmlPath = (0,
|
319
|
+
const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
|
318
320
|
await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
|
319
321
|
for (const screenFile of screenFiles) {
|
320
|
-
const filePath = (0,
|
322
|
+
const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
|
321
323
|
await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
|
322
324
|
}
|
323
325
|
const screenFilesConverted = screenFiles.map(screenFile => {
|
@@ -329,8 +331,8 @@ class StormCodegen {
|
|
329
331
|
type: codegen_1.AIFileTypes.WEB_SCREEN,
|
330
332
|
};
|
331
333
|
});
|
332
|
-
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
333
334
|
allFiles.push(...screenFilesConverted);
|
335
|
+
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
334
336
|
const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
|
335
337
|
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
336
338
|
const blockRef = block.uri;
|
@@ -346,7 +348,7 @@ class StormCodegen {
|
|
346
348
|
},
|
347
349
|
});
|
348
350
|
}
|
349
|
-
async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed,
|
351
|
+
async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles) {
|
350
352
|
let attempts = 0;
|
351
353
|
let validCode = false;
|
352
354
|
for (let i = 0; i <= 3; i++) {
|
@@ -360,35 +362,10 @@ class StormCodegen {
|
|
360
362
|
}
|
361
363
|
if (result && !result.valid) {
|
362
364
|
console.debug('Validation error:', result);
|
363
|
-
const
|
364
|
-
const
|
365
|
-
|
366
|
-
|
367
|
-
});
|
368
|
-
errorStream.on('data', (evt) => {
|
369
|
-
if (evt.type === 'ERROR_CLASSIFIER') {
|
370
|
-
// find the file that caused the error
|
371
|
-
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
372
|
-
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
373
|
-
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
374
|
-
if (!file) {
|
375
|
-
console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
|
376
|
-
}
|
377
|
-
// read the content of the file
|
378
|
-
const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
|
379
|
-
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
380
|
-
.map((e) => e.filename)
|
381
|
-
.join('\n')}\n---\n${content}`;
|
382
|
-
//console.log(`trying to fix the code in ${eventFileName}`);
|
383
|
-
//console.debug(`with the fix:\n${fix}`);
|
384
|
-
const code = this.codeFix(fix);
|
385
|
-
fixes.set((0, path_1.join)(basePath, eventFileName), code);
|
386
|
-
}
|
387
|
-
});
|
388
|
-
await errorStream.waitForDone();
|
389
|
-
for (const [filename, codePromise] of fixes) {
|
390
|
-
const code = await codePromise;
|
391
|
-
(0, fs_1.writeFileSync)(filename, code);
|
365
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
366
|
+
for (const [filename, fileErrors] of errors.entries()) {
|
367
|
+
// todo: only try to fix file if it is part of filesToBeFixed
|
368
|
+
await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
|
392
369
|
}
|
393
370
|
}
|
394
371
|
}
|
@@ -403,6 +380,127 @@ class StormCodegen {
|
|
403
380
|
console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
|
404
381
|
}
|
405
382
|
}
|
383
|
+
async tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator) {
|
384
|
+
console.log(`Processing ${filename}`);
|
385
|
+
const language = await codeGenerator.language();
|
386
|
+
const relevantFiles = allFiles.filter(file => file.type != codegen_1.AIFileTypes.IGNORE);
|
387
|
+
for (let attempts = 1; attempts <= 5; attempts++) {
|
388
|
+
if (fileErrors.length == 0) {
|
389
|
+
console.log(`No more errors for ${filename}`);
|
390
|
+
return;
|
391
|
+
}
|
392
|
+
console.log(`Errors in ${filename} - requesting error details`);
|
393
|
+
const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
|
394
|
+
console.log(`Get error details for ${filename} requesting code fixes`);
|
395
|
+
const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
|
396
|
+
const codeFixFiles = await this.codeFix(fix);
|
397
|
+
console.log(`Got fixed code for ${filename}`);
|
398
|
+
for (const codeFixFile of codeFixFiles) {
|
399
|
+
const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : (0, path_2.join)(basePath, codeFixFile.filename);
|
400
|
+
const existing = (0, fs_1.readFileSync)(filePath);
|
401
|
+
if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
|
402
|
+
console.log(`${filename} not changed by gemini`);
|
403
|
+
continue;
|
404
|
+
}
|
405
|
+
(0, fs_1.writeFileSync)(filePath, codeFixFile.content);
|
406
|
+
}
|
407
|
+
const result = await codeGenerator.validateForTarget(basePath);
|
408
|
+
if (result && result.valid) {
|
409
|
+
return;
|
410
|
+
}
|
411
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
412
|
+
fileErrors = errors.get(filename) ?? [];
|
413
|
+
}
|
414
|
+
}
|
415
|
+
async classifyErrors(errors, basePath) {
|
416
|
+
const errorStream = await stormClient_1.stormClient.createErrorClassification(errors, []);
|
417
|
+
const fixes = new Map();
|
418
|
+
this.out.on('aborted', () => {
|
419
|
+
errorStream.abort();
|
420
|
+
});
|
421
|
+
errorStream.on('data', (evt) => {
|
422
|
+
if (evt.type === 'ERROR_CLASSIFIER') {
|
423
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
424
|
+
const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
|
425
|
+
let existingFixes = fixes.get(eventFileName);
|
426
|
+
if (existingFixes) {
|
427
|
+
existingFixes.push(fix);
|
428
|
+
}
|
429
|
+
else {
|
430
|
+
fixes.set(eventFileName, [fix]);
|
431
|
+
}
|
432
|
+
}
|
433
|
+
});
|
434
|
+
await errorStream.waitForDone();
|
435
|
+
return fixes;
|
436
|
+
}
|
437
|
+
async getErrorDetailsForFile(basePath, filename, error, allFiles, language) {
|
438
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename); // to compensate when compiler returns absolute path
|
439
|
+
return new Promise(async (resolve, reject) => {
|
440
|
+
const request = {
|
441
|
+
"language": language,
|
442
|
+
"sourceFile": {
|
443
|
+
"filename": filename,
|
444
|
+
"content": (0, fs_1.readFileSync)(filePath, 'utf8')
|
445
|
+
},
|
446
|
+
"error": error,
|
447
|
+
"projectFiles": allFiles.map(f => f.filename)
|
448
|
+
};
|
449
|
+
const detailsStream = await stormClient_1.stormClient.createErrorDetails(JSON.stringify(request), []);
|
450
|
+
detailsStream.on('data', (evt) => {
|
451
|
+
if (evt.type === 'ERROR_DETAILS') {
|
452
|
+
resolve(evt.payload.files);
|
453
|
+
}
|
454
|
+
});
|
455
|
+
this.out.on('aborted', () => {
|
456
|
+
detailsStream.abort();
|
457
|
+
});
|
458
|
+
detailsStream.on('error', (err) => {
|
459
|
+
console.log("error", err);
|
460
|
+
reject(err);
|
461
|
+
});
|
462
|
+
await detailsStream.waitForDone();
|
463
|
+
});
|
464
|
+
}
|
465
|
+
createFixRequestForFile(basePath, filename, error, filesForContext, allFiles, language) {
|
466
|
+
const files = new Set(filesForContext);
|
467
|
+
files.add(filename);
|
468
|
+
const requestedFiles = Array.from(files).flatMap(file => {
|
469
|
+
if (fs.existsSync(file)) {
|
470
|
+
return file;
|
471
|
+
}
|
472
|
+
// file does not exist - look for similar
|
473
|
+
const candidateName = file.split('/').pop();
|
474
|
+
return allFiles
|
475
|
+
.filter(file => file.filename.split('/').pop() === candidateName)
|
476
|
+
.map(f => f.filename);
|
477
|
+
});
|
478
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
|
479
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
480
|
+
const affectedLine = this.getErrorLine(error, content);
|
481
|
+
const fixRequest = {
|
482
|
+
"language": language,
|
483
|
+
"filename": filename,
|
484
|
+
"errors": error.error,
|
485
|
+
"affectedLine": affectedLine,
|
486
|
+
"projectFiles": requestedFiles.map(filename => {
|
487
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
|
488
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
489
|
+
return { filename: filename, content: content };
|
490
|
+
}),
|
491
|
+
"conversationId": this.conversationId,
|
492
|
+
"sessionId": (0, uuid_1.v4)()
|
493
|
+
};
|
494
|
+
return JSON.stringify(fixRequest);
|
495
|
+
}
|
496
|
+
getErrorLine(errorDetails, sourceCode) {
|
497
|
+
const lines = sourceCode.split('\n');
|
498
|
+
const errorLine = lines[errorDetails.lineNumber - 1];
|
499
|
+
if (!errorLine) {
|
500
|
+
return "Error: Line number out of range.";
|
501
|
+
}
|
502
|
+
return errorLine;
|
503
|
+
}
|
406
504
|
removePrefix(prefix, str) {
|
407
505
|
if (str.startsWith(prefix)) {
|
408
506
|
return str.slice(prefix.length);
|
@@ -412,18 +510,19 @@ class StormCodegen {
|
|
412
510
|
/**
|
413
511
|
* Sends the code to the AI for a fix
|
414
512
|
*/
|
415
|
-
async codeFix(fix) {
|
513
|
+
async codeFix(fix, history) {
|
416
514
|
return new Promise(async (resolve, reject) => {
|
417
|
-
const fixStream = await stormClient_1.stormClient.createCodeFix(fix,
|
515
|
+
const fixStream = await stormClient_1.stormClient.createCodeFix(fix, history);
|
418
516
|
fixStream.on('data', (evt) => {
|
419
517
|
if (evt.type === 'CODE_FIX') {
|
420
|
-
resolve(evt.payload.content);
|
518
|
+
resolve(evt.payload.content.files);
|
421
519
|
}
|
422
520
|
});
|
423
521
|
this.out.on('aborted', () => {
|
424
522
|
fixStream.abort();
|
425
523
|
});
|
426
524
|
fixStream.on('error', (err) => {
|
525
|
+
console.log("error", err);
|
427
526
|
reject(err);
|
428
527
|
});
|
429
528
|
await fixStream.waitForDone();
|
@@ -455,7 +554,7 @@ class StormCodegen {
|
|
455
554
|
created: Date.now(),
|
456
555
|
payload: {
|
457
556
|
filename: file.filename,
|
458
|
-
path: (0,
|
557
|
+
path: (0, path_2.join)(basePath, file.filename),
|
459
558
|
content: file.content,
|
460
559
|
blockName: aiName,
|
461
560
|
blockRef: ref,
|
@@ -475,7 +574,7 @@ class StormCodegen {
|
|
475
574
|
created: Date.now(),
|
476
575
|
payload: {
|
477
576
|
filename: filename,
|
478
|
-
path: (0,
|
577
|
+
path: (0, path_2.join)(basePath, filename),
|
479
578
|
content: content,
|
480
579
|
blockName,
|
481
580
|
blockRef: ref,
|
@@ -111,14 +111,28 @@ export interface StormEventCodeFix {
|
|
111
111
|
reason: string;
|
112
112
|
created: number;
|
113
113
|
payload: {
|
114
|
-
|
115
|
-
|
114
|
+
content: {
|
115
|
+
files: StormEventErrorDetailsFile[];
|
116
|
+
};
|
116
117
|
};
|
117
118
|
}
|
118
119
|
export interface StormEventErrorClassifierInfo {
|
119
120
|
error: string;
|
120
121
|
filename: string;
|
121
|
-
|
122
|
+
lineNumber: number;
|
123
|
+
column: number;
|
124
|
+
}
|
125
|
+
export interface StormEventErrorDetailsFile {
|
126
|
+
filename: string;
|
127
|
+
content: string;
|
128
|
+
}
|
129
|
+
export interface StormEventErrorDetails {
|
130
|
+
type: 'ERROR_DETAILS';
|
131
|
+
reason: string;
|
132
|
+
created: number;
|
133
|
+
payload: {
|
134
|
+
files: string[];
|
135
|
+
};
|
122
136
|
}
|
123
137
|
export interface ScreenTemplate {
|
124
138
|
name: string;
|
@@ -218,4 +232,4 @@ export interface StormEventPhases {
|
|
218
232
|
phaseType: StormEventPhaseType;
|
219
233
|
};
|
220
234
|
}
|
221
|
-
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
|
235
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases;
|
@@ -11,6 +11,7 @@ declare class StormClient {
|
|
11
11
|
createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
|
12
12
|
createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
13
13
|
createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
14
|
+
createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
14
15
|
}
|
15
16
|
export declare const stormClient: StormClient;
|
16
17
|
export {};
|
@@ -100,5 +100,11 @@ class StormClient {
|
|
100
100
|
prompt,
|
101
101
|
});
|
102
102
|
}
|
103
|
+
createErrorDetails(prompt, history, conversationId) {
|
104
|
+
return this.send('/v2/code/errordetails', {
|
105
|
+
conversationId: conversationId,
|
106
|
+
prompt,
|
107
|
+
});
|
108
|
+
}
|
103
109
|
}
|
104
110
|
exports.stormClient = new StormClient();
|
@@ -26,6 +26,11 @@ export declare class StormCodegen {
|
|
26
26
|
*/
|
27
27
|
private processBlockCode;
|
28
28
|
private verifyAndFixCode;
|
29
|
+
private tryToFixFile;
|
30
|
+
private classifyErrors;
|
31
|
+
private getErrorDetailsForFile;
|
32
|
+
private createFixRequestForFile;
|
33
|
+
private getErrorLine;
|
29
34
|
removePrefix(prefix: string, str: string): string;
|
30
35
|
/**
|
31
36
|
* Sends the code to the AI for a fix
|
@@ -38,11 +38,13 @@ const event_parser_1 = require("./event-parser");
|
|
38
38
|
const stream_1 = require("./stream");
|
39
39
|
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
40
40
|
const promises_1 = require("fs/promises");
|
41
|
-
const path_1 =
|
41
|
+
const path_1 = __importDefault(require("path"));
|
42
|
+
const path_2 = __importStar(require("path"));
|
42
43
|
const node_os_1 = __importDefault(require("node:os"));
|
43
44
|
const fs_1 = require("fs");
|
44
|
-
const path_2 = __importDefault(require("path"));
|
45
45
|
const yaml_1 = __importDefault(require("yaml"));
|
46
|
+
const fs = __importStar(require("node:fs"));
|
47
|
+
const uuid_1 = require("uuid");
|
46
48
|
const SIMULATED_DELAY = 1000;
|
47
49
|
class SimulatedFileDelay {
|
48
50
|
file;
|
@@ -197,7 +199,7 @@ class StormCodegen {
|
|
197
199
|
...data,
|
198
200
|
payload: {
|
199
201
|
...data.payload,
|
200
|
-
path: (0,
|
202
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
201
203
|
blockName,
|
202
204
|
blockRef,
|
203
205
|
instanceId,
|
@@ -209,7 +211,7 @@ class StormCodegen {
|
|
209
211
|
...data,
|
210
212
|
payload: {
|
211
213
|
...data.payload,
|
212
|
-
path: (0,
|
214
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
213
215
|
blockName,
|
214
216
|
blockRef,
|
215
217
|
instanceId,
|
@@ -221,7 +223,7 @@ class StormCodegen {
|
|
221
223
|
...data,
|
222
224
|
payload: {
|
223
225
|
...data.payload,
|
224
|
-
path: (0,
|
226
|
+
path: (0, path_2.join)(basePath, data.payload.filename),
|
225
227
|
blockName,
|
226
228
|
blockRef,
|
227
229
|
instanceId,
|
@@ -241,7 +243,7 @@ class StormCodegen {
|
|
241
243
|
created: Date.now(),
|
242
244
|
payload: {
|
243
245
|
filename: data.payload.filename,
|
244
|
-
path: (0,
|
246
|
+
path: (0, path_2.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
|
245
247
|
content: data.payload.content,
|
246
248
|
blockRef: ref,
|
247
249
|
instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
|
@@ -297,7 +299,7 @@ class StormCodegen {
|
|
297
299
|
}
|
298
300
|
// Gather the context files for implementation. These will be all be passed to the AI
|
299
301
|
const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
|
300
|
-
// Send the service and UI templates to the AI. These will be
|
302
|
+
// Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
|
301
303
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
302
304
|
if (serviceFiles.length > 0) {
|
303
305
|
await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
@@ -307,17 +309,17 @@ class StormCodegen {
|
|
307
309
|
return;
|
308
310
|
}
|
309
311
|
for (const serviceFile of serviceFiles) {
|
310
|
-
const filePath = (0,
|
312
|
+
const filePath = (0, path_2.join)(basePath, serviceFile.filename);
|
311
313
|
await (0, promises_1.writeFile)(filePath, serviceFile.content);
|
312
314
|
}
|
313
315
|
for (const serviceFile of contextFiles) {
|
314
|
-
const filePath = (0,
|
316
|
+
const filePath = (0, path_2.join)(basePath, serviceFile.filename);
|
315
317
|
await (0, promises_1.writeFile)(filePath, serviceFile.content);
|
316
318
|
}
|
317
|
-
const kapetaYmlPath = (0,
|
319
|
+
const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
|
318
320
|
await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
|
319
321
|
for (const screenFile of screenFiles) {
|
320
|
-
const filePath = (0,
|
322
|
+
const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
|
321
323
|
await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
|
322
324
|
}
|
323
325
|
const screenFilesConverted = screenFiles.map(screenFile => {
|
@@ -329,8 +331,8 @@ class StormCodegen {
|
|
329
331
|
type: codegen_1.AIFileTypes.WEB_SCREEN,
|
330
332
|
};
|
331
333
|
});
|
332
|
-
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
333
334
|
allFiles.push(...screenFilesConverted);
|
335
|
+
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
334
336
|
const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
|
335
337
|
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
336
338
|
const blockRef = block.uri;
|
@@ -346,7 +348,7 @@ class StormCodegen {
|
|
346
348
|
},
|
347
349
|
});
|
348
350
|
}
|
349
|
-
async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed,
|
351
|
+
async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles) {
|
350
352
|
let attempts = 0;
|
351
353
|
let validCode = false;
|
352
354
|
for (let i = 0; i <= 3; i++) {
|
@@ -360,35 +362,10 @@ class StormCodegen {
|
|
360
362
|
}
|
361
363
|
if (result && !result.valid) {
|
362
364
|
console.debug('Validation error:', result);
|
363
|
-
const
|
364
|
-
const
|
365
|
-
|
366
|
-
|
367
|
-
});
|
368
|
-
errorStream.on('data', (evt) => {
|
369
|
-
if (evt.type === 'ERROR_CLASSIFIER') {
|
370
|
-
// find the file that caused the error
|
371
|
-
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
372
|
-
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
373
|
-
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
374
|
-
if (!file) {
|
375
|
-
console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
|
376
|
-
}
|
377
|
-
// read the content of the file
|
378
|
-
const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
|
379
|
-
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
380
|
-
.map((e) => e.filename)
|
381
|
-
.join('\n')}\n---\n${content}`;
|
382
|
-
//console.log(`trying to fix the code in ${eventFileName}`);
|
383
|
-
//console.debug(`with the fix:\n${fix}`);
|
384
|
-
const code = this.codeFix(fix);
|
385
|
-
fixes.set((0, path_1.join)(basePath, eventFileName), code);
|
386
|
-
}
|
387
|
-
});
|
388
|
-
await errorStream.waitForDone();
|
389
|
-
for (const [filename, codePromise] of fixes) {
|
390
|
-
const code = await codePromise;
|
391
|
-
(0, fs_1.writeFileSync)(filename, code);
|
365
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
366
|
+
for (const [filename, fileErrors] of errors.entries()) {
|
367
|
+
// todo: only try to fix file if it is part of filesToBeFixed
|
368
|
+
await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
|
392
369
|
}
|
393
370
|
}
|
394
371
|
}
|
@@ -403,6 +380,127 @@ class StormCodegen {
|
|
403
380
|
console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
|
404
381
|
}
|
405
382
|
}
|
383
|
+
async tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator) {
|
384
|
+
console.log(`Processing ${filename}`);
|
385
|
+
const language = await codeGenerator.language();
|
386
|
+
const relevantFiles = allFiles.filter(file => file.type != codegen_1.AIFileTypes.IGNORE);
|
387
|
+
for (let attempts = 1; attempts <= 5; attempts++) {
|
388
|
+
if (fileErrors.length == 0) {
|
389
|
+
console.log(`No more errors for ${filename}`);
|
390
|
+
return;
|
391
|
+
}
|
392
|
+
console.log(`Errors in ${filename} - requesting error details`);
|
393
|
+
const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
|
394
|
+
console.log(`Get error details for ${filename} requesting code fixes`);
|
395
|
+
const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
|
396
|
+
const codeFixFiles = await this.codeFix(fix);
|
397
|
+
console.log(`Got fixed code for ${filename}`);
|
398
|
+
for (const codeFixFile of codeFixFiles) {
|
399
|
+
const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : (0, path_2.join)(basePath, codeFixFile.filename);
|
400
|
+
const existing = (0, fs_1.readFileSync)(filePath);
|
401
|
+
if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
|
402
|
+
console.log(`${filename} not changed by gemini`);
|
403
|
+
continue;
|
404
|
+
}
|
405
|
+
(0, fs_1.writeFileSync)(filePath, codeFixFile.content);
|
406
|
+
}
|
407
|
+
const result = await codeGenerator.validateForTarget(basePath);
|
408
|
+
if (result && result.valid) {
|
409
|
+
return;
|
410
|
+
}
|
411
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
412
|
+
fileErrors = errors.get(filename) ?? [];
|
413
|
+
}
|
414
|
+
}
|
415
|
+
async classifyErrors(errors, basePath) {
|
416
|
+
const errorStream = await stormClient_1.stormClient.createErrorClassification(errors, []);
|
417
|
+
const fixes = new Map();
|
418
|
+
this.out.on('aborted', () => {
|
419
|
+
errorStream.abort();
|
420
|
+
});
|
421
|
+
errorStream.on('data', (evt) => {
|
422
|
+
if (evt.type === 'ERROR_CLASSIFIER') {
|
423
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
424
|
+
const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
|
425
|
+
let existingFixes = fixes.get(eventFileName);
|
426
|
+
if (existingFixes) {
|
427
|
+
existingFixes.push(fix);
|
428
|
+
}
|
429
|
+
else {
|
430
|
+
fixes.set(eventFileName, [fix]);
|
431
|
+
}
|
432
|
+
}
|
433
|
+
});
|
434
|
+
await errorStream.waitForDone();
|
435
|
+
return fixes;
|
436
|
+
}
|
437
|
+
async getErrorDetailsForFile(basePath, filename, error, allFiles, language) {
|
438
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename); // to compensate when compiler returns absolute path
|
439
|
+
return new Promise(async (resolve, reject) => {
|
440
|
+
const request = {
|
441
|
+
"language": language,
|
442
|
+
"sourceFile": {
|
443
|
+
"filename": filename,
|
444
|
+
"content": (0, fs_1.readFileSync)(filePath, 'utf8')
|
445
|
+
},
|
446
|
+
"error": error,
|
447
|
+
"projectFiles": allFiles.map(f => f.filename)
|
448
|
+
};
|
449
|
+
const detailsStream = await stormClient_1.stormClient.createErrorDetails(JSON.stringify(request), []);
|
450
|
+
detailsStream.on('data', (evt) => {
|
451
|
+
if (evt.type === 'ERROR_DETAILS') {
|
452
|
+
resolve(evt.payload.files);
|
453
|
+
}
|
454
|
+
});
|
455
|
+
this.out.on('aborted', () => {
|
456
|
+
detailsStream.abort();
|
457
|
+
});
|
458
|
+
detailsStream.on('error', (err) => {
|
459
|
+
console.log("error", err);
|
460
|
+
reject(err);
|
461
|
+
});
|
462
|
+
await detailsStream.waitForDone();
|
463
|
+
});
|
464
|
+
}
|
465
|
+
createFixRequestForFile(basePath, filename, error, filesForContext, allFiles, language) {
|
466
|
+
const files = new Set(filesForContext);
|
467
|
+
files.add(filename);
|
468
|
+
const requestedFiles = Array.from(files).flatMap(file => {
|
469
|
+
if (fs.existsSync(file)) {
|
470
|
+
return file;
|
471
|
+
}
|
472
|
+
// file does not exist - look for similar
|
473
|
+
const candidateName = file.split('/').pop();
|
474
|
+
return allFiles
|
475
|
+
.filter(file => file.filename.split('/').pop() === candidateName)
|
476
|
+
.map(f => f.filename);
|
477
|
+
});
|
478
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
|
479
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
480
|
+
const affectedLine = this.getErrorLine(error, content);
|
481
|
+
const fixRequest = {
|
482
|
+
"language": language,
|
483
|
+
"filename": filename,
|
484
|
+
"errors": error.error,
|
485
|
+
"affectedLine": affectedLine,
|
486
|
+
"projectFiles": requestedFiles.map(filename => {
|
487
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
|
488
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
489
|
+
return { filename: filename, content: content };
|
490
|
+
}),
|
491
|
+
"conversationId": this.conversationId,
|
492
|
+
"sessionId": (0, uuid_1.v4)()
|
493
|
+
};
|
494
|
+
return JSON.stringify(fixRequest);
|
495
|
+
}
|
496
|
+
getErrorLine(errorDetails, sourceCode) {
|
497
|
+
const lines = sourceCode.split('\n');
|
498
|
+
const errorLine = lines[errorDetails.lineNumber - 1];
|
499
|
+
if (!errorLine) {
|
500
|
+
return "Error: Line number out of range.";
|
501
|
+
}
|
502
|
+
return errorLine;
|
503
|
+
}
|
406
504
|
removePrefix(prefix, str) {
|
407
505
|
if (str.startsWith(prefix)) {
|
408
506
|
return str.slice(prefix.length);
|
@@ -412,18 +510,19 @@ class StormCodegen {
|
|
412
510
|
/**
|
413
511
|
* Sends the code to the AI for a fix
|
414
512
|
*/
|
415
|
-
async codeFix(fix) {
|
513
|
+
async codeFix(fix, history) {
|
416
514
|
return new Promise(async (resolve, reject) => {
|
417
|
-
const fixStream = await stormClient_1.stormClient.createCodeFix(fix,
|
515
|
+
const fixStream = await stormClient_1.stormClient.createCodeFix(fix, history);
|
418
516
|
fixStream.on('data', (evt) => {
|
419
517
|
if (evt.type === 'CODE_FIX') {
|
420
|
-
resolve(evt.payload.content);
|
518
|
+
resolve(evt.payload.content.files);
|
421
519
|
}
|
422
520
|
});
|
423
521
|
this.out.on('aborted', () => {
|
424
522
|
fixStream.abort();
|
425
523
|
});
|
426
524
|
fixStream.on('error', (err) => {
|
525
|
+
console.log("error", err);
|
427
526
|
reject(err);
|
428
527
|
});
|
429
528
|
await fixStream.waitForDone();
|
@@ -455,7 +554,7 @@ class StormCodegen {
|
|
455
554
|
created: Date.now(),
|
456
555
|
payload: {
|
457
556
|
filename: file.filename,
|
458
|
-
path: (0,
|
557
|
+
path: (0, path_2.join)(basePath, file.filename),
|
459
558
|
content: file.content,
|
460
559
|
blockName: aiName,
|
461
560
|
blockRef: ref,
|
@@ -475,7 +574,7 @@ class StormCodegen {
|
|
475
574
|
created: Date.now(),
|
476
575
|
payload: {
|
477
576
|
filename: filename,
|
478
|
-
path: (0,
|
577
|
+
path: (0, path_2.join)(basePath, filename),
|
479
578
|
content: content,
|
480
579
|
blockName,
|
481
580
|
blockRef: ref,
|
@@ -111,14 +111,28 @@ export interface StormEventCodeFix {
|
|
111
111
|
reason: string;
|
112
112
|
created: number;
|
113
113
|
payload: {
|
114
|
-
|
115
|
-
|
114
|
+
content: {
|
115
|
+
files: StormEventErrorDetailsFile[];
|
116
|
+
};
|
116
117
|
};
|
117
118
|
}
|
118
119
|
export interface StormEventErrorClassifierInfo {
|
119
120
|
error: string;
|
120
121
|
filename: string;
|
121
|
-
|
122
|
+
lineNumber: number;
|
123
|
+
column: number;
|
124
|
+
}
|
125
|
+
export interface StormEventErrorDetailsFile {
|
126
|
+
filename: string;
|
127
|
+
content: string;
|
128
|
+
}
|
129
|
+
export interface StormEventErrorDetails {
|
130
|
+
type: 'ERROR_DETAILS';
|
131
|
+
reason: string;
|
132
|
+
created: number;
|
133
|
+
payload: {
|
134
|
+
files: string[];
|
135
|
+
};
|
122
136
|
}
|
123
137
|
export interface ScreenTemplate {
|
124
138
|
name: string;
|
@@ -218,4 +232,4 @@ export interface StormEventPhases {
|
|
218
232
|
phaseType: StormEventPhaseType;
|
219
233
|
};
|
220
234
|
}
|
221
|
-
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
|
235
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases;
|
@@ -11,6 +11,7 @@ declare class StormClient {
|
|
11
11
|
createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
|
12
12
|
createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
13
13
|
createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
14
|
+
createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
14
15
|
}
|
15
16
|
export declare const stormClient: StormClient;
|
16
17
|
export {};
|
@@ -100,5 +100,11 @@ class StormClient {
|
|
100
100
|
prompt,
|
101
101
|
});
|
102
102
|
}
|
103
|
+
createErrorDetails(prompt, history, conversationId) {
|
104
|
+
return this.send('/v2/code/errordetails', {
|
105
|
+
conversationId: conversationId,
|
106
|
+
prompt,
|
107
|
+
});
|
108
|
+
}
|
103
109
|
}
|
104
110
|
exports.stormClient = new StormClient();
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.52.1",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -50,7 +50,7 @@
|
|
50
50
|
},
|
51
51
|
"homepage": "https://github.com/kapetacom/local-cluster-service#readme",
|
52
52
|
"dependencies": {
|
53
|
-
"@kapeta/codegen": "^1.
|
53
|
+
"@kapeta/codegen": "^1.5.0",
|
54
54
|
"@kapeta/config-mapper": "^1.2.1",
|
55
55
|
"@kapeta/kaplang-core": "^1.17.1",
|
56
56
|
"@kapeta/local-cluster-config": "^0.4.2",
|
package/src/storm/codegen.ts
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
|
6
|
-
import {
|
6
|
+
import {Definition} from '@kapeta/local-cluster-config';
|
7
7
|
import {
|
8
8
|
AIFileTypes,
|
9
9
|
BlockCodeGenerator,
|
@@ -13,22 +13,36 @@ import {
|
|
13
13
|
GeneratedResult,
|
14
14
|
MODE_CREATE_ONLY,
|
15
15
|
} from '@kapeta/codegen';
|
16
|
-
import {
|
17
|
-
import {
|
18
|
-
import {
|
19
|
-
import {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
16
|
+
import {BlockDefinition} from '@kapeta/schemas';
|
17
|
+
import {codeGeneratorManager} from '../codeGeneratorManager';
|
18
|
+
import {STORM_ID, stormClient} from './stormClient';
|
19
|
+
import {
|
20
|
+
StormEvent,
|
21
|
+
StormEventErrorDetailsFile,
|
22
|
+
StormEventFileChunk,
|
23
|
+
StormEventFileDone,
|
24
|
+
StormEventFileLogical
|
25
|
+
} from './events';
|
26
|
+
import {BlockDefinitionInfo, StormEventParser} from './event-parser';
|
27
|
+
import {ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream} from './stream';
|
28
|
+
import {KapetaURI, parseKapetaUri} from '@kapeta/nodejs-utils';
|
29
|
+
import {writeFile} from 'fs/promises';
|
30
|
+
import path from 'path';
|
31
|
+
import Path, {join} from 'path';
|
25
32
|
import os from 'node:os';
|
26
|
-
import {
|
27
|
-
import Path from 'path';
|
33
|
+
import {readFileSync, writeFileSync} from 'fs';
|
28
34
|
import YAML from "yaml";
|
35
|
+
import * as fs from "node:fs";
|
36
|
+
import {v4 as uuidv4} from 'uuid';
|
29
37
|
|
30
38
|
type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
|
31
39
|
|
40
|
+
interface ErrorClassification {
|
41
|
+
error: string;
|
42
|
+
lineNumber: number;
|
43
|
+
column: number;
|
44
|
+
}
|
45
|
+
|
32
46
|
const SIMULATED_DELAY = 1000;
|
33
47
|
|
34
48
|
class SimulatedFileDelay {
|
@@ -321,7 +335,7 @@ export class StormCodegen {
|
|
321
335
|
(file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
|
322
336
|
);
|
323
337
|
|
324
|
-
// Send the service and UI templates to the AI. These will be
|
338
|
+
// Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
|
325
339
|
const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
|
326
340
|
if (serviceFiles.length > 0) {
|
327
341
|
await this.processTemplates(
|
@@ -366,8 +380,9 @@ export class StormCodegen {
|
|
366
380
|
type: AIFileTypes.WEB_SCREEN,
|
367
381
|
};
|
368
382
|
});
|
369
|
-
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
370
383
|
allFiles.push(...screenFilesConverted);
|
384
|
+
|
385
|
+
const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
|
371
386
|
const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
|
372
387
|
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
373
388
|
|
@@ -389,7 +404,7 @@ export class StormCodegen {
|
|
389
404
|
codeGenerator: CodeGenerator,
|
390
405
|
basePath: string,
|
391
406
|
filesToBeFixed: StormFileInfo[],
|
392
|
-
|
407
|
+
allFiles: StormFileInfo[]
|
393
408
|
) {
|
394
409
|
let attempts = 0;
|
395
410
|
let validCode = false;
|
@@ -405,46 +420,18 @@ export class StormCodegen {
|
|
405
420
|
|
406
421
|
if (result && !result.valid) {
|
407
422
|
console.debug('Validation error:', result);
|
408
|
-
|
409
|
-
const
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
});
|
414
|
-
|
415
|
-
errorStream.on('data', (evt) => {
|
416
|
-
if (evt.type === 'ERROR_CLASSIFIER') {
|
417
|
-
// find the file that caused the error
|
418
|
-
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
419
|
-
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
420
|
-
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
421
|
-
if (!file) {
|
422
|
-
console.log(
|
423
|
-
`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
|
424
|
-
);
|
425
|
-
}
|
426
|
-
// read the content of the file
|
427
|
-
const content = readFileSync(join(basePath, eventFileName), 'utf8');
|
428
|
-
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
429
|
-
.map((e) => e.filename)
|
430
|
-
.join('\n')}\n---\n${content}`;
|
431
|
-
//console.log(`trying to fix the code in ${eventFileName}`);
|
432
|
-
//console.debug(`with the fix:\n${fix}`);
|
433
|
-
const code = this.codeFix(fix);
|
434
|
-
fixes.set(join(basePath, eventFileName), code);
|
435
|
-
}
|
436
|
-
});
|
437
|
-
|
438
|
-
await errorStream.waitForDone();
|
439
|
-
for (const [filename, codePromise] of fixes) {
|
440
|
-
const code = await codePromise;
|
441
|
-
writeFileSync(filename, code);
|
423
|
+
|
424
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
425
|
+
for (const [filename, fileErrors] of errors.entries()) {
|
426
|
+
// todo: only try to fix file if it is part of filesToBeFixed
|
427
|
+
await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
|
442
428
|
}
|
443
429
|
}
|
444
430
|
} catch (e) {
|
445
431
|
console.error('Error:', e);
|
446
432
|
}
|
447
433
|
}
|
434
|
+
|
448
435
|
if (validCode) {
|
449
436
|
console.log(`Validation successful after ${attempts} attempts`);
|
450
437
|
} else {
|
@@ -452,6 +439,152 @@ export class StormCodegen {
|
|
452
439
|
}
|
453
440
|
}
|
454
441
|
|
442
|
+
private async tryToFixFile(basePath: string, filename: string, fileErrors: ErrorClassification[], allFiles: StormFileInfo[], codeGenerator: CodeGenerator) {
|
443
|
+
console.log(`Processing ${filename}`);
|
444
|
+
const language = await codeGenerator.language();
|
445
|
+
const relevantFiles = allFiles.filter(file => file.type != AIFileTypes.IGNORE);
|
446
|
+
|
447
|
+
for (let attempts = 1; attempts <= 5; attempts++) {
|
448
|
+
if (fileErrors.length == 0) {
|
449
|
+
console.log(`No more errors for ${filename}`);
|
450
|
+
return;
|
451
|
+
}
|
452
|
+
|
453
|
+
console.log(`Errors in ${filename} - requesting error details`);
|
454
|
+
const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
|
455
|
+
console.log(`Get error details for ${filename} requesting code fixes`);
|
456
|
+
|
457
|
+
const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
|
458
|
+
const codeFixFiles = await this.codeFix(fix);
|
459
|
+
console.log(`Got fixed code for ${filename}`);
|
460
|
+
for (const codeFixFile of codeFixFiles) {
|
461
|
+
const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : join(basePath, codeFixFile.filename);
|
462
|
+
const existing = readFileSync(filePath);
|
463
|
+
if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
|
464
|
+
console.log(`${filename} not changed by gemini`);
|
465
|
+
continue;
|
466
|
+
}
|
467
|
+
writeFileSync(filePath, codeFixFile.content);
|
468
|
+
}
|
469
|
+
|
470
|
+
const result = await codeGenerator.validateForTarget(basePath);
|
471
|
+
if (result && result.valid) {
|
472
|
+
return;
|
473
|
+
}
|
474
|
+
|
475
|
+
const errors = await this.classifyErrors(result.error, basePath);
|
476
|
+
fileErrors = errors.get(filename) ?? [];
|
477
|
+
}
|
478
|
+
}
|
479
|
+
|
480
|
+
private async classifyErrors(errors: string, basePath: string): Promise<Map<string, ErrorClassification[]>> {
|
481
|
+
const errorStream = await stormClient.createErrorClassification(errors, []);
|
482
|
+
const fixes = new Map<string, ErrorClassification[]>();
|
483
|
+
|
484
|
+
this.out.on('aborted', () => {
|
485
|
+
errorStream.abort();
|
486
|
+
});
|
487
|
+
|
488
|
+
errorStream.on('data', (evt) => {
|
489
|
+
if (evt.type === 'ERROR_CLASSIFIER') {
|
490
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
491
|
+
const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
|
492
|
+
|
493
|
+
let existingFixes = fixes.get(eventFileName);
|
494
|
+
if (existingFixes) {
|
495
|
+
existingFixes.push(fix);
|
496
|
+
} else {
|
497
|
+
fixes.set(eventFileName, [fix]);
|
498
|
+
}
|
499
|
+
}
|
500
|
+
});
|
501
|
+
|
502
|
+
await errorStream.waitForDone();
|
503
|
+
|
504
|
+
return fixes;
|
505
|
+
}
|
506
|
+
|
507
|
+
private async getErrorDetailsForFile(basePath: string, filename: string, error: ErrorClassification, allFiles: StormFileInfo[], language: string): Promise<string[]> {
|
508
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename); // to compensate when compiler returns absolute path
|
509
|
+
return new Promise<string[]>(async (resolve, reject) => {
|
510
|
+
const request = {
|
511
|
+
"language": language,
|
512
|
+
"sourceFile": {
|
513
|
+
"filename": filename,
|
514
|
+
"content": readFileSync(filePath, 'utf8')
|
515
|
+
},
|
516
|
+
"error": error,
|
517
|
+
"projectFiles": allFiles.map(f => f.filename)
|
518
|
+
};
|
519
|
+
|
520
|
+
const detailsStream = await stormClient.createErrorDetails(JSON.stringify(request), []);
|
521
|
+
detailsStream.on('data', (evt) => {
|
522
|
+
if (evt.type === 'ERROR_DETAILS') {
|
523
|
+
resolve(evt.payload.files);
|
524
|
+
}
|
525
|
+
});
|
526
|
+
this.out.on('aborted', () => {
|
527
|
+
detailsStream.abort();
|
528
|
+
});
|
529
|
+
detailsStream.on('error', (err) => {
|
530
|
+
console.log("error", err);
|
531
|
+
reject(err);
|
532
|
+
});
|
533
|
+
await detailsStream.waitForDone();
|
534
|
+
});
|
535
|
+
}
|
536
|
+
|
537
|
+
private createFixRequestForFile(basePath: string, filename: string, error: ErrorClassification, filesForContext: string[], allFiles: StormFileInfo[], language: string): string {
|
538
|
+
const files = new Set(filesForContext);
|
539
|
+
files.add(filename);
|
540
|
+
|
541
|
+
const requestedFiles = Array.from(files).flatMap(file => {
|
542
|
+
if (fs.existsSync(file)) {
|
543
|
+
return file;
|
544
|
+
}
|
545
|
+
|
546
|
+
// file does not exist - look for similar
|
547
|
+
const candidateName = file.split('/').pop();
|
548
|
+
return allFiles
|
549
|
+
.filter(file => file.filename.split('/').pop() === candidateName)
|
550
|
+
.map(f => f.filename);
|
551
|
+
});
|
552
|
+
|
553
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
|
554
|
+
const content = readFileSync(filePath, 'utf8');
|
555
|
+
const affectedLine = this.getErrorLine(error, content);
|
556
|
+
|
557
|
+
const fixRequest = {
|
558
|
+
"language": language,
|
559
|
+
"filename": filename,
|
560
|
+
"errors": error.error,
|
561
|
+
"affectedLine": affectedLine,
|
562
|
+
"projectFiles": requestedFiles.map(filename => {
|
563
|
+
const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
|
564
|
+
const content = readFileSync(filePath, 'utf8');
|
565
|
+
return { filename: filename, content: content };
|
566
|
+
}),
|
567
|
+
"conversationId": this.conversationId,
|
568
|
+
"sessionId": uuidv4()
|
569
|
+
};
|
570
|
+
|
571
|
+
return JSON.stringify(fixRequest);
|
572
|
+
}
|
573
|
+
|
574
|
+
private getErrorLine(
|
575
|
+
errorDetails: ErrorClassification,
|
576
|
+
sourceCode: string
|
577
|
+
): string {
|
578
|
+
const lines = sourceCode.split('\n');
|
579
|
+
const errorLine = lines[errorDetails.lineNumber - 1];
|
580
|
+
|
581
|
+
if (!errorLine) {
|
582
|
+
return "Error: Line number out of range.";
|
583
|
+
}
|
584
|
+
|
585
|
+
return errorLine;
|
586
|
+
}
|
587
|
+
|
455
588
|
removePrefix(prefix: string, str: string): string {
|
456
589
|
if (str.startsWith(prefix)) {
|
457
590
|
return str.slice(prefix.length);
|
@@ -462,18 +595,19 @@ export class StormCodegen {
|
|
462
595
|
/**
|
463
596
|
* Sends the code to the AI for a fix
|
464
597
|
*/
|
465
|
-
private async codeFix(fix: string): Promise<
|
466
|
-
return new Promise<
|
467
|
-
const fixStream = await stormClient.createCodeFix(fix,
|
598
|
+
private async codeFix(fix: string, history?: ConversationItem[]): Promise<StormEventErrorDetailsFile[]> {
|
599
|
+
return new Promise<StormEventErrorDetailsFile[]>(async (resolve, reject) => {
|
600
|
+
const fixStream = await stormClient.createCodeFix(fix, history);
|
468
601
|
fixStream.on('data', (evt) => {
|
469
602
|
if (evt.type === 'CODE_FIX') {
|
470
|
-
resolve(evt.payload.content);
|
603
|
+
resolve(evt.payload.content.files);
|
471
604
|
}
|
472
605
|
});
|
473
606
|
this.out.on('aborted', () => {
|
474
607
|
fixStream.abort();
|
475
608
|
});
|
476
609
|
fixStream.on('error', (err) => {
|
610
|
+
console.log("error", err);
|
477
611
|
reject(err);
|
478
612
|
});
|
479
613
|
await fixStream.waitForDone();
|
package/src/storm/events.ts
CHANGED
@@ -139,14 +139,31 @@ export interface StormEventCodeFix {
|
|
139
139
|
reason: string;
|
140
140
|
created: number;
|
141
141
|
payload: {
|
142
|
-
|
143
|
-
|
142
|
+
content: {
|
143
|
+
files: StormEventErrorDetailsFile[]
|
144
|
+
}
|
144
145
|
};
|
145
146
|
}
|
147
|
+
|
146
148
|
export interface StormEventErrorClassifierInfo {
|
147
149
|
error: string;
|
148
150
|
filename: string;
|
149
|
-
|
151
|
+
lineNumber: number;
|
152
|
+
column: number;
|
153
|
+
}
|
154
|
+
|
155
|
+
export interface StormEventErrorDetailsFile {
|
156
|
+
filename: string;
|
157
|
+
content: string;
|
158
|
+
}
|
159
|
+
|
160
|
+
export interface StormEventErrorDetails {
|
161
|
+
type: 'ERROR_DETAILS';
|
162
|
+
reason: string;
|
163
|
+
created: number;
|
164
|
+
payload: {
|
165
|
+
files: string[]
|
166
|
+
};
|
150
167
|
}
|
151
168
|
|
152
169
|
export interface ScreenTemplate {
|
@@ -280,5 +297,6 @@ export type StormEvent =
|
|
280
297
|
| StormEventDefinitionChange
|
281
298
|
| StormEventErrorClassifier
|
282
299
|
| StormEventCodeFix
|
300
|
+
| StormEventErrorDetails
|
283
301
|
| StormEventBlockReady
|
284
302
|
| StormEventPhases;
|
package/src/storm/stormClient.ts
CHANGED
@@ -125,12 +125,20 @@ class StormClient {
|
|
125
125
|
prompt,
|
126
126
|
});
|
127
127
|
}
|
128
|
+
|
128
129
|
public createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string) {
|
129
130
|
return this.send('/v2/code/fix', {
|
130
131
|
conversationId: conversationId,
|
131
132
|
prompt,
|
132
133
|
});
|
133
134
|
}
|
135
|
+
|
136
|
+
public createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string) {
|
137
|
+
return this.send('/v2/code/errordetails', {
|
138
|
+
conversationId: conversationId,
|
139
|
+
prompt,
|
140
|
+
});
|
141
|
+
}
|
134
142
|
}
|
135
143
|
|
136
144
|
export const stormClient = new StormClient();
|