@kapeta/local-cluster-service 0.75.0 → 0.76.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.
@@ -7,7 +7,6 @@ import { Definition } from '@kapeta/local-cluster-config';
7
7
  import {
8
8
  AIFileTypes,
9
9
  BlockCodeGenerator,
10
- CodeGenerator,
11
10
  CodeWriter,
12
11
  GeneratedFile,
13
12
  GeneratedResult,
@@ -21,20 +20,18 @@ import { STORM_ID, StormClient } from './stormClient';
21
20
  import {
22
21
  StormEvent,
23
22
  StormEventBlockStatusType,
24
- StormEventErrorDetailsFile,
25
23
  StormEventFileChunk,
26
24
  StormEventFileDone,
27
25
  StormEventFileLogical,
28
26
  StormEventScreen,
29
27
  } from './events';
30
28
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
31
- import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
29
+ import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
32
30
  import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
33
31
  import { writeFile } from 'fs/promises';
34
32
  import path from 'path';
35
33
  import Path, { join } from 'path';
36
34
  import os from 'node:os';
37
- import { readFileSync, writeFileSync, existsSync } from 'fs';
38
35
  import YAML from 'yaml';
39
36
  import { PREDEFINED_BLOCKS } from './predefined';
40
37
  import { Archetype } from './archetype';
@@ -46,12 +43,6 @@ type ImplementationGenerator<T = StormFileImplementationPrompt> = (
46
43
  conversationId?: string
47
44
  ) => Promise<StormStream>;
48
45
 
49
- interface ErrorClassification {
50
- error: string;
51
- lineNumber: number;
52
- column: number;
53
- }
54
-
55
46
  const SIMULATED_DELAY = 1000;
56
47
  const ENABLE_SIMULATED_DELAY = false;
57
48
  class SimulatedFileDelay {
@@ -371,7 +362,7 @@ export class StormCodegen {
371
362
  });
372
363
  const uiEvents = [];
373
364
 
374
- const stormClient = new StormClient(this.uiSystemId);
365
+ const stormClient = new StormClient(blockUri.handle, this.uiSystemId);
375
366
  // generate screens
376
367
  if (uiTemplates.length) {
377
368
  const screenStream = await stormClient.listScreens({
@@ -540,249 +531,6 @@ export class StormCodegen {
540
531
  } satisfies StormEvent);
541
532
  }
542
533
 
543
- private async verifyAndFixCode(
544
- blockUri: KapetaURI,
545
- blockName: string,
546
- codeGenerator: CodeGenerator,
547
- basePath: string,
548
- filesToBeFixed: StormFileInfo[],
549
- allFiles: StormFileInfo[]
550
- ) {
551
- let attempts = 0;
552
- let validCode = false;
553
- for (let i = 0; i <= 3; i++) {
554
- attempts++;
555
- try {
556
- console.log(`Validating the code in ${basePath} attempt #${attempts}`);
557
- const result = await codeGenerator.validateForTarget(basePath);
558
- if (result && result.valid) {
559
- validCode = true;
560
- break;
561
- }
562
-
563
- if (result && !result.valid) {
564
- console.debug('Validation error:', result);
565
-
566
- this.emitBlockStatus(blockUri, blockName, StormEventBlockStatusType.PLANNING_FIX);
567
-
568
- const errors = await this.classifyErrors(result.error, basePath);
569
-
570
- if (errors.size > 0) {
571
- this.emitBlockStatus(blockUri, blockName, StormEventBlockStatusType.FIXING);
572
-
573
- const promises = Array.from(errors.entries()).map(([filename, fileErrors]) => {
574
- if (filesToBeFixed.some((file) => file.filename === filename)) {
575
- return this.tryToFixFile(
576
- blockUri,
577
- blockName,
578
- basePath,
579
- filename,
580
- fileErrors,
581
- allFiles,
582
- codeGenerator
583
- );
584
- }
585
- });
586
-
587
- await Promise.all(promises);
588
- }
589
-
590
- this.emitBlockStatus(blockUri, blockName, StormEventBlockStatusType.FIX_DONE);
591
- }
592
- } catch (e) {
593
- console.error('Error:', e);
594
- }
595
- }
596
-
597
- if (validCode) {
598
- console.log(`Validation successful after ${attempts} attempts`);
599
- } else {
600
- console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
601
- }
602
- }
603
-
604
- private async tryToFixFile(
605
- blockUri: KapetaURI,
606
- blockName: string,
607
- basePath: string,
608
- filename: string,
609
- fileErrors: ErrorClassification[],
610
- allFiles: StormFileInfo[],
611
- codeGenerator: CodeGenerator
612
- ) {
613
- console.log(`Processing ${filename}`);
614
- const language = await codeGenerator.language();
615
- const relevantFiles = allFiles.filter((file) => file.type != AIFileTypes.IGNORE);
616
-
617
- for (let attempts = 1; attempts <= 5; attempts++) {
618
- if (fileErrors.length == 0) {
619
- console.log(`No more errors for ${filename}`);
620
- return;
621
- }
622
-
623
- console.log(`Errors in ${filename} - requesting error details`);
624
- const filesForContext = await this.getErrorDetailsForFile(
625
- basePath,
626
- filename,
627
- fileErrors[0],
628
- relevantFiles,
629
- language
630
- );
631
- console.log(`Get error details for ${filename} requesting code fixes`);
632
-
633
- const fix = this.createFixRequestForFile(
634
- basePath,
635
- filename,
636
- fileErrors[0],
637
- filesForContext,
638
- relevantFiles,
639
- language
640
- );
641
- const codeFixFile = await this.codeFix(blockUri, blockName, fix);
642
- console.log(`Got fixed code for ${filename}`);
643
- const filePath =
644
- codeFixFile.filename.indexOf(basePath) > -1
645
- ? codeFixFile.filename
646
- : join(basePath, codeFixFile.filename);
647
- const existing = readFileSync(filePath);
648
- if (
649
- existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')
650
- ) {
651
- console.log(`${filename} not changed by gemini`);
652
- continue;
653
- }
654
- writeFileSync(filePath, codeFixFile.content);
655
-
656
- const result = await codeGenerator.validateForTarget(basePath);
657
- if (result && result.valid) {
658
- return;
659
- }
660
-
661
- const errors = await this.classifyErrors(result.error, basePath);
662
- fileErrors = errors.get(filename) ?? [];
663
- }
664
- }
665
-
666
- private async classifyErrors(errors: string, basePath: string): Promise<Map<string, ErrorClassification[]>> {
667
- const stormClient = new StormClient(this.uiSystemId);
668
- const errorStream = await stormClient.createErrorClassification(errors, []);
669
- const fixes = new Map<string, ErrorClassification[]>();
670
-
671
- this.out.on('aborted', () => {
672
- errorStream.abort();
673
- });
674
-
675
- errorStream.on('data', (evt) => {
676
- if (evt.type === 'ERROR_CLASSIFIER') {
677
- const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
678
- const fix = {
679
- error: evt.payload.error,
680
- lineNumber: evt.payload.lineNumber,
681
- column: evt.payload.column,
682
- };
683
-
684
- let existingFixes = fixes.get(eventFileName);
685
- if (existingFixes) {
686
- existingFixes.push(fix);
687
- } else {
688
- fixes.set(eventFileName, [fix]);
689
- }
690
- }
691
- });
692
-
693
- await errorStream.waitForDone();
694
-
695
- return fixes;
696
- }
697
-
698
- private async getErrorDetailsForFile(
699
- basePath: string,
700
- filename: string,
701
- error: ErrorClassification,
702
- allFiles: StormFileInfo[],
703
- language: string
704
- ): Promise<string[]> {
705
- const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename); // to compensate when compiler returns absolute path
706
- return new Promise<string[]>(async (resolve, reject) => {
707
- const request = {
708
- language: language,
709
- sourceFile: {
710
- filename: filename,
711
- content: readFileSync(filePath, 'utf8'),
712
- },
713
- error: error,
714
- projectFiles: allFiles.map((f) => f.filename),
715
- };
716
- const stormClient = new StormClient(this.uiSystemId);
717
- const detailsStream = await stormClient.createErrorDetails(JSON.stringify(request), []);
718
- detailsStream.on('data', (evt) => {
719
- if (evt.type === 'ERROR_DETAILS') {
720
- resolve(evt.payload.files);
721
- }
722
- reject(new Error('Error details: Unexpected event [' + evt.type + ']'));
723
- });
724
- this.out.on('aborted', () => {
725
- detailsStream.abort();
726
- reject(new Error('aborted'));
727
- });
728
- detailsStream.on('error', (err) => {
729
- reject(err);
730
- });
731
- await detailsStream.waitForDone();
732
- });
733
- }
734
-
735
- private createFixRequestForFile(
736
- basePath: string,
737
- filename: string,
738
- error: ErrorClassification,
739
- filesForContext: string[],
740
- allFiles: StormFileInfo[],
741
- language: string
742
- ): string {
743
- const files = new Set(filesForContext);
744
- files.add(filename);
745
-
746
- const requestedFiles = Array.from(files).flatMap((file) => {
747
- if (existsSync(file)) {
748
- return file;
749
- }
750
-
751
- // file does not exist - look for similar
752
- const candidateName = file.split('/').pop();
753
- return allFiles.filter((file) => file.filename.split('/').pop() === candidateName).map((f) => f.filename);
754
- });
755
-
756
- const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
757
- const content = readFileSync(filePath, 'utf8');
758
- const affectedLine = this.getErrorLine(error, content);
759
-
760
- const fixRequest = {
761
- language: language,
762
- filename: filename,
763
- error: error.error,
764
- affectedLine: affectedLine,
765
- projectFiles: requestedFiles.map((filename) => {
766
- const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
767
- const content = readFileSync(filePath, 'utf8');
768
- return { filename: filename, content: content };
769
- }),
770
- };
771
-
772
- return JSON.stringify(fixRequest);
773
- }
774
-
775
- private getErrorLine(errorDetails: ErrorClassification, sourceCode: string): string {
776
- const lines = sourceCode.split('\n');
777
- const errorLine = lines[errorDetails.lineNumber - 1];
778
-
779
- if (!errorLine) {
780
- return 'Error: Line number out of range.';
781
- }
782
-
783
- return errorLine;
784
- }
785
-
786
534
  removePrefix(prefix: string, str: string): string {
787
535
  if (str.startsWith(prefix)) {
788
536
  return str.slice(prefix.length);
@@ -790,49 +538,6 @@ export class StormCodegen {
790
538
  return str;
791
539
  }
792
540
 
793
- /**
794
- * Sends the code to the AI for a fix
795
- */
796
- private async codeFix(
797
- blockUri: KapetaURI,
798
- blockName: string,
799
- fix: string,
800
- history?: ConversationItem[]
801
- ): Promise<StormEventErrorDetailsFile> {
802
- return new Promise<StormEventErrorDetailsFile>(async (resolve, reject) => {
803
- try {
804
- const stormClient = new StormClient(this.uiSystemId);
805
- const fixStream = await stormClient.createCodeFix(fix, history, this.conversationId);
806
- let resolved = false;
807
- fixStream.on('data', (evt) => {
808
- if (this.handleFileEvents(blockUri, blockName, evt)) {
809
- return;
810
- }
811
- this.handleFileDoneOutput(blockUri, blockName, evt);
812
-
813
- if (evt.type === 'CODE_FIX') {
814
- resolved = true;
815
- resolve(evt.payload);
816
- }
817
- });
818
- this.out.on('aborted', () => {
819
- fixStream.abort();
820
- reject(new Error('aborted'));
821
- });
822
- fixStream.on('error', (err) => {
823
- reject(err);
824
- });
825
- fixStream.on('end', () => {
826
- if (!resolved) {
827
- reject(new Error('Code fix never returned a valid event'));
828
- }
829
- });
830
- } catch (e) {
831
- reject(e);
832
- }
833
- });
834
- }
835
-
836
541
  /**
837
542
  * Emits the text-based files to the stream
838
543
  */
@@ -152,6 +152,7 @@ router.post('/ui/conversations/:systemId/append', async (req: KapetaBodyRequest,
152
152
 
153
153
  router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest, res: Response) => {
154
154
  const systemId = req.params.systemId as string;
155
+ const handle = req.params.handle as string;
155
156
  const srcDir = getSystemBaseDir(systemId);
156
157
  const destDir = getSystemBaseImplDir(systemId);
157
158
 
@@ -162,7 +163,7 @@ router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest
162
163
  sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENT_APIS));
163
164
 
164
165
  const pagesFromDisk = readFilesAndContent(srcDir);
165
- const client = new StormClient(systemId);
166
+ const client = new StormClient(handle, systemId)
166
167
  const pagesWithImplementation = await client.replaceMockWithAPICall({
167
168
  pages: pagesFromDisk,
168
169
  systemId: systemId,
@@ -206,7 +207,7 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req: KapetaBody
206
207
  //res.set(ConversationIdHeader, systemId);
207
208
 
208
209
  //sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENT_APIS));
209
- const client = new StormClient(systemId);
210
+ const client = new StormClient(handle, systemId);
210
211
  try {
211
212
  const pagesFromDisk = readFilesAndContent(srcDir);
212
213
  const pagesWithImplementation = await client.replaceMockWithAPICall({
@@ -306,7 +307,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
306
307
 
307
308
  const parentConversationId = systemId ?? '';
308
309
 
309
- const queue = new PageQueue(parentConversationId, '', 5);
310
+ const queue = new PageQueue("", parentConversationId, '', 5);
310
311
  onRequestAborted(req, res, () => {
311
312
  queue.cancel();
312
313
  });
@@ -348,7 +349,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
348
349
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
349
350
 
350
351
  const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
351
- const client = new StormClient(conversationId); //todo is this correct we are using the landing page getConversationId down below as well
352
+ const client = new StormClient(handle, conversationId); //todo is this correct we are using the landing page getConversationId down below as well
352
353
  const landingPagesStream = await client.createUILandingPages(aiRequest, conversationId);
353
354
 
354
355
  onRequestAborted(req, res, () => {
@@ -411,7 +412,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
411
412
  systemPrompt.resolve(aiRequest.prompt);
412
413
  });
413
414
 
414
- const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
415
+ const pageQueue = new PageQueue(handle, systemId, await systemPrompt.promise, 5);
415
416
  onRequestAborted(req, res, () => {
416
417
  pageQueue.cancel();
417
418
  });
@@ -457,7 +458,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
457
458
  (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
458
459
 
459
460
  const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
460
- const stormClient = new StormClient(outerConversationId);
461
+ const stormClient = new StormClient(handle, outerConversationId);
461
462
  // Get user journeys
462
463
  const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
463
464
 
@@ -572,7 +573,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
572
573
  shellsStream.abort();
573
574
  });
574
575
 
575
- const queue = new PageQueue(outerConversationId, systemPrompt, 5);
576
+ const queue = new PageQueue(handle, outerConversationId, systemPrompt, 5);
576
577
  queue.setUiTheme(theme);
577
578
 
578
579
  shellsStream.on('data', (data: StormEvent) => {
@@ -679,7 +680,7 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
679
680
  const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
680
681
  const storagePrefix = systemId ? systemId + '_' : 'mock_';
681
682
 
682
- const queue = new PageQueue(systemId!, '', 5);
683
+ const queue = new PageQueue("",systemId!, '', 5);
683
684
 
684
685
  onRequestAborted(req, res, () => {
685
686
  queue.cancel();
@@ -753,7 +754,7 @@ router.post('/ui/vote', async (req: KapetaBodyRequest, res: Response) => {
753
754
  const aiRequest: UIPageVoteRequest = JSON.parse(req.stringBody ?? '{}');
754
755
  const { topic, vote, mainConversationId } = aiRequest;
755
756
  try {
756
- const stormClient = new StormClient(mainConversationId);
757
+ const stormClient = new StormClient("", mainConversationId);
757
758
  await stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
758
759
  } catch (e: any) {
759
760
  res.status(500).send({ error: e.message });
@@ -765,7 +766,7 @@ router.post('/ui/get-vote', async (req: KapetaBodyRequest, res: Response) => {
765
766
  const aiRequest: UIPageGetVoteRequest = JSON.parse(req.stringBody ?? '{}');
766
767
  const { topic, mainConversationId } = aiRequest;
767
768
  try {
768
- const stormClient = new StormClient(mainConversationId);
769
+ const stormClient = new StormClient("", mainConversationId);
769
770
  const vote = await stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
770
771
  res.send({ vote });
771
772
  } catch (e: any) {
@@ -797,7 +798,7 @@ async function handleAll(req: KapetaBodyRequest, res: Response) {
797
798
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
798
799
 
799
800
  const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
800
- const stormClient = new StormClient(systemId);
801
+ const stormClient = new StormClient(handle, systemId);
801
802
  const metaStream = await stormClient.createMetadata(aiRequest, conversationId);
802
803
 
803
804
  onRequestAborted(req, res, () => {
@@ -25,6 +25,7 @@ export const STORM_ID = 'storm';
25
25
 
26
26
  export const ConversationIdHeader = 'Conversation-Id';
27
27
  export const SystemIdHeader = 'System-Id';
28
+ export const HandleHeader = 'Handle';
28
29
 
29
30
  export interface UIShellsPrompt {
30
31
  theme?: string;
@@ -89,9 +90,11 @@ export interface BasePromptRequest {
89
90
  export class StormClient {
90
91
  private readonly _baseUrl: string;
91
92
  private readonly _systemId: string;
92
- constructor(systemId?: string) {
93
+ private readonly _handle: string;
94
+ constructor(handle: string, systemId?: string) {
93
95
  this._baseUrl = getRemoteUrl('ai-service', 'https://ai.kapeta.com');
94
96
  this._systemId = systemId || "";
97
+ this._handle = handle;
95
98
  }
96
99
 
97
100
  private async createOptions(
@@ -115,6 +118,9 @@ export class StormClient {
115
118
  if (this._systemId) {
116
119
  headers[SystemIdHeader] = this._systemId
117
120
  }
121
+ if (this._handle) {
122
+ headers[HandleHeader] = this._handle
123
+ }
118
124
  return {
119
125
  url,
120
126
  method: method,
@@ -251,6 +257,7 @@ export class StormClient {
251
257
  body: JSON.stringify(prompt.pages),
252
258
  headers: {
253
259
  'systemId': prompt.systemId,
260
+ 'conversationId': prompt.systemId,
254
261
  },
255
262
  });
256
263
  return (await response.json()) as HTMLPage[];
@@ -149,14 +149,14 @@ export class StormService {
149
149
  },
150
150
  ['.']
151
151
  );
152
- const stormClient = new StormClient(systemId);
152
+ const stormClient = new StormClient(handle, systemId);
153
153
  await stormClient.uploadSystem(handle, systemId, await fs.readFile(tarballFile));
154
154
  }
155
155
 
156
156
  async installProjectById(handle: string, systemId: string) {
157
157
  const tarballFile = this.getConversationTarball(systemId);
158
158
  const destDir = path.dirname(tarballFile);
159
- const stormClient = new StormClient(systemId);
159
+ const stormClient = new StormClient(handle, systemId);
160
160
  const buffer = await stormClient.downloadSystem(handle, systemId);
161
161
  await fs.mkdir(destDir, { recursive: true });
162
162
  await fs.writeFile(tarballFile, buffer);