@kapeta/local-cluster-service 0.52.0 → 0.52.2
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/routes.js +3 -16
- package/dist/cjs/src/storm/stormClient.d.ts +1 -0
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/cjs/src/storm/stream.js +10 -4
- 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/routes.js +3 -16
- package/dist/esm/src/storm/stormClient.d.ts +1 -0
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/dist/esm/src/storm/stream.js +10 -4
- package/package.json +1 -1
- package/src/storm/codegen.ts +187 -53
- package/src/storm/events.ts +21 -3
- package/src/storm/routes.ts +3 -18
- package/src/storm/stormClient.ts +8 -0
- package/src/storm/stream.ts +10 -5
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/routes.ts
CHANGED
@@ -156,7 +156,6 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
|
|
156
156
|
const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
|
157
157
|
res.send(asset);
|
158
158
|
} catch (err: any) {
|
159
|
-
console.error('err');
|
160
159
|
res.status(500).send({ error: err.message });
|
161
160
|
}
|
162
161
|
});
|
@@ -199,30 +198,16 @@ function sendError(err: Error, res: Response) {
|
|
199
198
|
}
|
200
199
|
}
|
201
200
|
function waitForStormStream(result: StormStream) {
|
202
|
-
return
|
203
|
-
result.on('error', (err) => {
|
204
|
-
reject(err);
|
205
|
-
});
|
206
|
-
|
207
|
-
result.on('end', () => {
|
208
|
-
resolve();
|
209
|
-
});
|
210
|
-
});
|
201
|
+
return result.waitForDone();
|
211
202
|
}
|
212
203
|
|
213
204
|
function streamStormPartialResponse(result: StormStream, res: Response) {
|
214
|
-
return new Promise<void>((resolve
|
205
|
+
return new Promise<void>((resolve) => {
|
215
206
|
result.on('data', (data) => {
|
216
207
|
sendEvent(res, data);
|
217
208
|
});
|
218
209
|
|
219
|
-
result.
|
220
|
-
reject(err);
|
221
|
-
});
|
222
|
-
|
223
|
-
result.on('end', () => {
|
224
|
-
resolve();
|
225
|
-
});
|
210
|
+
resolve(result.waitForDone());
|
226
211
|
});
|
227
212
|
}
|
228
213
|
|
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();
|
package/src/storm/stream.ts
CHANGED
@@ -60,13 +60,18 @@ export class StormStream extends EventEmitter {
|
|
60
60
|
|
61
61
|
waitForDone() {
|
62
62
|
return new Promise<void>((resolve, reject) => {
|
63
|
-
|
63
|
+
const errorHandler = (err: any) => {
|
64
|
+
this.removeListener('error', errorHandler);
|
65
|
+
this.removeListener('end', endHandler);
|
64
66
|
reject(err);
|
65
|
-
}
|
66
|
-
|
67
|
-
|
67
|
+
};
|
68
|
+
const endHandler = () => {
|
69
|
+
this.removeListener('error', errorHandler);
|
70
|
+
this.removeListener('end', endHandler);
|
68
71
|
resolve();
|
69
|
-
}
|
72
|
+
};
|
73
|
+
this.once('error', errorHandler);
|
74
|
+
this.once('end', endHandler);
|
70
75
|
});
|
71
76
|
}
|
72
77
|
|