@kapeta/local-cluster-service 0.75.0 → 0.76.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.
@@ -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({
@@ -292,8 +293,9 @@ router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Respons
292
293
  res.status(200).json({ status: 'ok' });
293
294
  });
294
295
 
295
- router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
296
+ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
296
297
  try {
298
+ const handle = req.params.handle as string;
297
299
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
298
300
  const systemId = req.headers[SystemIdHeader.toLowerCase()] as string | undefined;
299
301
 
@@ -306,7 +308,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
306
308
 
307
309
  const parentConversationId = systemId ?? '';
308
310
 
309
- const queue = new PageQueue(parentConversationId, '', 5);
311
+ const queue = new PageQueue(handle, parentConversationId, '', 5);
310
312
  onRequestAborted(req, res, () => {
311
313
  queue.cancel();
312
314
  });
@@ -348,7 +350,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
348
350
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
349
351
 
350
352
  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
353
+ const client = new StormClient(handle, conversationId); //todo is this correct we are using the landing page getConversationId down below as well
352
354
  const landingPagesStream = await client.createUILandingPages(aiRequest, conversationId);
353
355
 
354
356
  onRequestAborted(req, res, () => {
@@ -411,7 +413,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
411
413
  systemPrompt.resolve(aiRequest.prompt);
412
414
  });
413
415
 
414
- const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
416
+ const pageQueue = new PageQueue(handle, systemId, await systemPrompt.promise, 5);
415
417
  onRequestAborted(req, res, () => {
416
418
  pageQueue.cancel();
417
419
  });
@@ -457,7 +459,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
457
459
  (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
458
460
 
459
461
  const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
460
- const stormClient = new StormClient(outerConversationId);
462
+ const stormClient = new StormClient(handle, outerConversationId);
461
463
  // Get user journeys
462
464
  const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
463
465
 
@@ -572,7 +574,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
572
574
  shellsStream.abort();
573
575
  });
574
576
 
575
- const queue = new PageQueue(outerConversationId, systemPrompt, 5);
577
+ const queue = new PageQueue(handle, outerConversationId, systemPrompt, 5);
576
578
  queue.setUiTheme(theme);
577
579
 
578
580
  shellsStream.on('data', (data: StormEvent) => {
@@ -671,15 +673,16 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
671
673
  }
672
674
  });
673
675
 
674
- router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
676
+ router.post('/:handle/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
675
677
  try {
678
+ const handle = req.params.handle as string;
676
679
  const systemId = (req.headers[SystemIdHeader.toLowerCase()] ||
677
680
  req.headers[ConversationIdHeader.toLowerCase()]) as string | undefined;
678
681
 
679
682
  const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
680
683
  const storagePrefix = systemId ? systemId + '_' : 'mock_';
681
684
 
682
- const queue = new PageQueue(systemId!, '', 5);
685
+ const queue = new PageQueue(handle, systemId!, '', 5);
683
686
 
684
687
  onRequestAborted(req, res, () => {
685
688
  queue.cancel();
@@ -753,7 +756,7 @@ router.post('/ui/vote', async (req: KapetaBodyRequest, res: Response) => {
753
756
  const aiRequest: UIPageVoteRequest = JSON.parse(req.stringBody ?? '{}');
754
757
  const { topic, vote, mainConversationId } = aiRequest;
755
758
  try {
756
- const stormClient = new StormClient(mainConversationId);
759
+ const stormClient = new StormClient("", mainConversationId);
757
760
  await stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
758
761
  } catch (e: any) {
759
762
  res.status(500).send({ error: e.message });
@@ -765,7 +768,7 @@ router.post('/ui/get-vote', async (req: KapetaBodyRequest, res: Response) => {
765
768
  const aiRequest: UIPageGetVoteRequest = JSON.parse(req.stringBody ?? '{}');
766
769
  const { topic, mainConversationId } = aiRequest;
767
770
  try {
768
- const stormClient = new StormClient(mainConversationId);
771
+ const stormClient = new StormClient("", mainConversationId);
769
772
  const vote = await stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
770
773
  res.send({ vote });
771
774
  } catch (e: any) {
@@ -797,7 +800,7 @@ async function handleAll(req: KapetaBodyRequest, res: Response) {
797
800
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
798
801
 
799
802
  const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
800
- const stormClient = new StormClient(systemId);
803
+ const stormClient = new StormClient(handle, systemId);
801
804
  const metaStream = await stormClient.createMetadata(aiRequest, conversationId);
802
805
 
803
806
  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);