@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.
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import { Definition } from '@kapeta/local-cluster-config';
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 { BlockDefinition } from '@kapeta/schemas';
17
- import { codeGeneratorManager } from '../codeGeneratorManager';
18
- import { STORM_ID, stormClient } from './stormClient';
19
- import { StormEvent, StormEventFileChunk, StormEventFileDone, StormEventFileLogical } from './events';
20
- import { BlockDefinitionInfo, StormEventParser } from './event-parser';
21
- import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
22
- import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
23
- import { writeFile } from 'fs/promises';
24
- import path, { join } from 'path';
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 { readFileSync, writeFileSync } from 'fs';
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 send one-by-one in addition to the context files
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
- knownFiles: StormFileInfo[]
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
- const errorStream = await stormClient.createErrorClassification(result.error, []);
409
- const fixes = new Map<string, Promise<string>>();
410
-
411
- this.out.on('aborted', () => {
412
- errorStream.abort();
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<string> {
466
- return new Promise<string>(async (resolve, reject) => {
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();
@@ -139,14 +139,31 @@ export interface StormEventCodeFix {
139
139
  reason: string;
140
140
  created: number;
141
141
  payload: {
142
- filename: string;
143
- content: string;
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
- potentialFix: string;
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;
@@ -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 new Promise<void>((resolve, reject) => {
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, reject) => {
205
+ return new Promise<void>((resolve) => {
215
206
  result.on('data', (data) => {
216
207
  sendEvent(res, data);
217
208
  });
218
209
 
219
- result.on('error', (err) => {
220
- reject(err);
221
- });
222
-
223
- result.on('end', () => {
224
- resolve();
225
- });
210
+ resolve(result.waitForDone());
226
211
  });
227
212
  }
228
213
 
@@ -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();
@@ -60,13 +60,18 @@ export class StormStream extends EventEmitter {
60
60
 
61
61
  waitForDone() {
62
62
  return new Promise<void>((resolve, reject) => {
63
- this.on('error', (err) => {
63
+ const errorHandler = (err: any) => {
64
+ this.removeListener('error', errorHandler);
65
+ this.removeListener('end', endHandler);
64
66
  reject(err);
65
- });
66
-
67
- this.on('end', () => {
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