@kapeta/local-cluster-service 0.74.2 → 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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/src/middleware/stringBody.js +2 -1
- package/dist/cjs/src/storm/PageGenerator.d.ts +2 -1
- package/dist/cjs/src/storm/PageGenerator.js +5 -3
- package/dist/cjs/src/storm/codegen.d.ts +0 -10
- package/dist/cjs/src/storm/codegen.js +1 -202
- package/dist/cjs/src/storm/routes.js +28 -11
- package/dist/cjs/src/storm/stormClient.d.ts +3 -1
- package/dist/cjs/src/storm/stormClient.js +9 -2
- package/dist/cjs/src/stormService.d.ts +7 -0
- package/dist/cjs/src/stormService.js +25 -4
- package/dist/esm/src/middleware/stringBody.js +2 -1
- package/dist/esm/src/storm/PageGenerator.d.ts +2 -1
- package/dist/esm/src/storm/PageGenerator.js +5 -3
- package/dist/esm/src/storm/codegen.d.ts +0 -10
- package/dist/esm/src/storm/codegen.js +1 -202
- package/dist/esm/src/storm/routes.js +28 -11
- package/dist/esm/src/storm/stormClient.d.ts +3 -1
- package/dist/esm/src/storm/stormClient.js +9 -2
- package/dist/esm/src/stormService.d.ts +7 -0
- package/dist/esm/src/stormService.js +25 -4
- package/package.json +1 -1
- package/src/middleware/stringBody.ts +2 -1
- package/src/storm/PageGenerator.ts +5 -3
- package/src/storm/codegen.ts +2 -297
- package/src/storm/routes.ts +29 -11
- package/src/storm/stormClient.ts +8 -1
- package/src/stormService.ts +38 -6
package/src/storm/codegen.ts
CHANGED
@@ -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 {
|
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
|
*/
|
package/src/storm/routes.ts
CHANGED
@@ -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({
|
@@ -260,6 +261,23 @@ router.post('/ui/systems/:handle/:systemId/upload', async (req: KapetaBodyReques
|
|
260
261
|
res.send({ ok: true });
|
261
262
|
});
|
262
263
|
|
264
|
+
router.put('/ui/systems/:handle/:systemId/thumbnail', async (req: KapetaBodyRequest, res: Response) => {
|
265
|
+
const systemId = req.params.systemId as string;
|
266
|
+
await stormService.saveThumbnail(systemId, req.body as Buffer);
|
267
|
+
res.send({ ok: true });
|
268
|
+
});
|
269
|
+
|
270
|
+
router.get('/ui/systems/:handle/:systemId/thumbnail.png', async (req: KapetaBodyRequest, res: Response) => {
|
271
|
+
const systemId = req.params.systemId as string;
|
272
|
+
const thumbnail = await stormService.getThumbnail(systemId);
|
273
|
+
if (thumbnail) {
|
274
|
+
res.set('Content-Type', 'image/png');
|
275
|
+
res.send(thumbnail);
|
276
|
+
} else {
|
277
|
+
res.status(404).send({ error: 'No thumbnail found' });
|
278
|
+
}
|
279
|
+
});
|
280
|
+
|
263
281
|
router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response) => {
|
264
282
|
const systemId = req.params.systemId as string | undefined;
|
265
283
|
if (!systemId) {
|
@@ -289,7 +307,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
289
307
|
|
290
308
|
const parentConversationId = systemId ?? '';
|
291
309
|
|
292
|
-
const queue = new PageQueue(parentConversationId, '', 5);
|
310
|
+
const queue = new PageQueue("", parentConversationId, '', 5);
|
293
311
|
onRequestAborted(req, res, () => {
|
294
312
|
queue.cancel();
|
295
313
|
});
|
@@ -331,7 +349,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
331
349
|
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
332
350
|
|
333
351
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
334
|
-
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
|
335
353
|
const landingPagesStream = await client.createUILandingPages(aiRequest, conversationId);
|
336
354
|
|
337
355
|
onRequestAborted(req, res, () => {
|
@@ -394,7 +412,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
394
412
|
systemPrompt.resolve(aiRequest.prompt);
|
395
413
|
});
|
396
414
|
|
397
|
-
const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
|
415
|
+
const pageQueue = new PageQueue(handle, systemId, await systemPrompt.promise, 5);
|
398
416
|
onRequestAborted(req, res, () => {
|
399
417
|
pageQueue.cancel();
|
400
418
|
});
|
@@ -440,7 +458,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
440
458
|
(req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
|
441
459
|
|
442
460
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
443
|
-
const stormClient = new StormClient(outerConversationId);
|
461
|
+
const stormClient = new StormClient(handle, outerConversationId);
|
444
462
|
// Get user journeys
|
445
463
|
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
446
464
|
|
@@ -555,7 +573,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
555
573
|
shellsStream.abort();
|
556
574
|
});
|
557
575
|
|
558
|
-
const queue = new PageQueue(outerConversationId, systemPrompt, 5);
|
576
|
+
const queue = new PageQueue(handle, outerConversationId, systemPrompt, 5);
|
559
577
|
queue.setUiTheme(theme);
|
560
578
|
|
561
579
|
shellsStream.on('data', (data: StormEvent) => {
|
@@ -662,7 +680,7 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
|
|
662
680
|
const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
|
663
681
|
const storagePrefix = systemId ? systemId + '_' : 'mock_';
|
664
682
|
|
665
|
-
const queue = new PageQueue(systemId!, '', 5);
|
683
|
+
const queue = new PageQueue("",systemId!, '', 5);
|
666
684
|
|
667
685
|
onRequestAborted(req, res, () => {
|
668
686
|
queue.cancel();
|
@@ -736,7 +754,7 @@ router.post('/ui/vote', async (req: KapetaBodyRequest, res: Response) => {
|
|
736
754
|
const aiRequest: UIPageVoteRequest = JSON.parse(req.stringBody ?? '{}');
|
737
755
|
const { topic, vote, mainConversationId } = aiRequest;
|
738
756
|
try {
|
739
|
-
const stormClient = new StormClient(mainConversationId);
|
757
|
+
const stormClient = new StormClient("", mainConversationId);
|
740
758
|
await stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
|
741
759
|
} catch (e: any) {
|
742
760
|
res.status(500).send({ error: e.message });
|
@@ -748,7 +766,7 @@ router.post('/ui/get-vote', async (req: KapetaBodyRequest, res: Response) => {
|
|
748
766
|
const aiRequest: UIPageGetVoteRequest = JSON.parse(req.stringBody ?? '{}');
|
749
767
|
const { topic, mainConversationId } = aiRequest;
|
750
768
|
try {
|
751
|
-
const stormClient = new StormClient(mainConversationId);
|
769
|
+
const stormClient = new StormClient("", mainConversationId);
|
752
770
|
const vote = await stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
|
753
771
|
res.send({ vote });
|
754
772
|
} catch (e: any) {
|
@@ -780,7 +798,7 @@ async function handleAll(req: KapetaBodyRequest, res: Response) {
|
|
780
798
|
const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
|
781
799
|
|
782
800
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
783
|
-
const stormClient = new StormClient(systemId);
|
801
|
+
const stormClient = new StormClient(handle, systemId);
|
784
802
|
const metaStream = await stormClient.createMetadata(aiRequest, conversationId);
|
785
803
|
|
786
804
|
onRequestAborted(req, res, () => {
|
package/src/storm/stormClient.ts
CHANGED
@@ -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
|
-
|
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[];
|
package/src/stormService.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import fs from 'fs/promises';
|
2
|
-
import { glob } from 'glob';
|
2
|
+
import { glob, Path } from 'glob';
|
3
3
|
import { filesystemManager } from './filesystemManager';
|
4
4
|
import path from 'path';
|
5
5
|
import { existsSync } from 'fs';
|
@@ -16,6 +16,10 @@ export class StormService {
|
|
16
16
|
return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'system.tar.gz');
|
17
17
|
}
|
18
18
|
|
19
|
+
private getThumbnailFile(conversationId: string) {
|
20
|
+
return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'thumbnail.png');
|
21
|
+
}
|
22
|
+
|
19
23
|
async listRemoteConversations() {
|
20
24
|
// i.e. conversations from org / user on registry
|
21
25
|
return [];
|
@@ -25,13 +29,24 @@ export class StormService {
|
|
25
29
|
const systemsFolder = path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems');
|
26
30
|
const eventFiles = await glob('*/events.ndjson', {
|
27
31
|
cwd: systemsFolder,
|
28
|
-
|
32
|
+
stat: true,
|
33
|
+
withFileTypes: true,
|
29
34
|
});
|
30
35
|
// Returns list of UUIDs - probably want to make it more useful than that
|
31
|
-
const conversations: {
|
36
|
+
const conversations: {
|
37
|
+
id: string;
|
38
|
+
description: string;
|
39
|
+
title: string;
|
40
|
+
url?: string;
|
41
|
+
lastModified?: number;
|
42
|
+
createdAt?: number;
|
43
|
+
thumbnail?: string;
|
44
|
+
}[] = [];
|
45
|
+
// Sort by modification time, newest first
|
46
|
+
eventFiles.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
32
47
|
for (const file of eventFiles) {
|
33
48
|
try {
|
34
|
-
const nldContents = await fs.readFile(file
|
49
|
+
const nldContents = await fs.readFile(file.fullpath(), 'utf8');
|
35
50
|
const events = nldContents.split('\n').map((e) => JSON.parse(e)) as {
|
36
51
|
// | { type: 'USER'; event: any } // IS stupid!
|
37
52
|
type: 'AI';
|
@@ -79,6 +94,9 @@ export class StormService {
|
|
79
94
|
description: initialPrompt,
|
80
95
|
title: title || 'New system',
|
81
96
|
url,
|
97
|
+
lastModified: file.mtimeMs,
|
98
|
+
createdAt: file.birthtimeMs,
|
99
|
+
thumbnail: existsSync(this.getThumbnailFile(id)) ? `thumbnail.png?v=${file.mtimeMs}` : undefined,
|
82
100
|
});
|
83
101
|
} catch (e) {
|
84
102
|
console.error('Failed to load conversation at %s', file, e);
|
@@ -131,14 +149,14 @@ export class StormService {
|
|
131
149
|
},
|
132
150
|
['.']
|
133
151
|
);
|
134
|
-
const stormClient = new StormClient(systemId);
|
152
|
+
const stormClient = new StormClient(handle, systemId);
|
135
153
|
await stormClient.uploadSystem(handle, systemId, await fs.readFile(tarballFile));
|
136
154
|
}
|
137
155
|
|
138
156
|
async installProjectById(handle: string, systemId: string) {
|
139
157
|
const tarballFile = this.getConversationTarball(systemId);
|
140
158
|
const destDir = path.dirname(tarballFile);
|
141
|
-
const stormClient = new StormClient(systemId);
|
159
|
+
const stormClient = new StormClient(handle, systemId);
|
142
160
|
const buffer = await stormClient.downloadSystem(handle, systemId);
|
143
161
|
await fs.mkdir(destDir, { recursive: true });
|
144
162
|
await fs.writeFile(tarballFile, buffer);
|
@@ -148,6 +166,20 @@ export class StormService {
|
|
148
166
|
});
|
149
167
|
await fs.unlink(tarballFile);
|
150
168
|
}
|
169
|
+
|
170
|
+
async saveThumbnail(systemId: string, thumbnail: Buffer) {
|
171
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
172
|
+
await fs.mkdir(path.dirname(thumbnailFile), { recursive: true });
|
173
|
+
await fs.writeFile(thumbnailFile, thumbnail);
|
174
|
+
}
|
175
|
+
|
176
|
+
async getThumbnail(systemId: string) {
|
177
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
178
|
+
if (existsSync(thumbnailFile)) {
|
179
|
+
return fs.readFile(thumbnailFile);
|
180
|
+
}
|
181
|
+
return null;
|
182
|
+
}
|
151
183
|
}
|
152
184
|
|
153
185
|
export default new StormService();
|