@kapeta/local-cluster-service 0.48.4 → 0.49.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/.vscode/launch.json +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/cjs/src/assetManager.d.ts +1 -1
- package/dist/cjs/src/assetManager.js +5 -3
- package/dist/cjs/src/filesystem/routes.js +10 -0
- package/dist/cjs/src/storm/codegen.d.ts +9 -1
- package/dist/cjs/src/storm/codegen.js +111 -8
- package/dist/cjs/src/storm/event-parser.d.ts +1 -1
- package/dist/cjs/src/storm/event-parser.js +3 -1
- package/dist/cjs/src/storm/events.d.ts +32 -1
- package/dist/cjs/src/storm/routes.js +26 -1
- package/dist/cjs/src/storm/stormClient.d.ts +3 -1
- package/dist/cjs/src/storm/stormClient.js +12 -0
- package/dist/cjs/src/storm/stream.d.ts +6 -0
- package/dist/esm/src/assetManager.d.ts +1 -1
- package/dist/esm/src/assetManager.js +5 -3
- package/dist/esm/src/filesystem/routes.js +10 -0
- package/dist/esm/src/storm/codegen.d.ts +9 -1
- package/dist/esm/src/storm/codegen.js +111 -8
- package/dist/esm/src/storm/event-parser.d.ts +1 -1
- package/dist/esm/src/storm/event-parser.js +3 -1
- package/dist/esm/src/storm/events.d.ts +32 -1
- package/dist/esm/src/storm/routes.js +26 -1
- package/dist/esm/src/storm/stormClient.d.ts +3 -1
- package/dist/esm/src/storm/stormClient.js +12 -0
- package/dist/esm/src/storm/stream.d.ts +6 -0
- package/package.json +1 -1
- package/src/assetManager.ts +6 -4
- package/src/filesystem/routes.ts +10 -0
- package/src/storm/codegen.ts +132 -12
- package/src/storm/event-parser.ts +4 -2
- package/src/storm/events.ts +38 -1
- package/src/storm/routes.ts +39 -2
- package/src/storm/stormClient.ts +13 -1
- package/src/storm/stream.ts +7 -0
@@ -36,25 +36,31 @@ const codeGeneratorManager_1 = require("../codeGeneratorManager");
|
|
36
36
|
const stormClient_1 = require("./stormClient");
|
37
37
|
const event_parser_1 = require("./event-parser");
|
38
38
|
const stream_1 = require("./stream");
|
39
|
+
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
39
40
|
const promises_1 = require("fs/promises");
|
40
41
|
const path_1 = __importStar(require("path"));
|
41
42
|
const node_os_1 = __importDefault(require("node:os"));
|
43
|
+
const fs_1 = require("fs");
|
44
|
+
const path_2 = __importDefault(require("path"));
|
42
45
|
class StormCodegen {
|
43
46
|
userPrompt;
|
44
47
|
blocks;
|
45
48
|
out = new stream_1.StormStream();
|
46
49
|
events;
|
47
50
|
tmpDir;
|
48
|
-
|
51
|
+
conversationId;
|
52
|
+
constructor(conversationId, userPrompt, blocks, events) {
|
49
53
|
this.userPrompt = userPrompt;
|
50
54
|
this.blocks = blocks;
|
51
55
|
this.events = events;
|
52
|
-
this.tmpDir = node_os_1.default.tmpdir();
|
56
|
+
this.tmpDir = path_2.default.join(node_os_1.default.tmpdir(), conversationId);
|
57
|
+
this.conversationId = conversationId;
|
53
58
|
}
|
54
59
|
async process() {
|
55
|
-
|
56
|
-
|
57
|
-
}
|
60
|
+
const promises = this.blocks.map((block) => {
|
61
|
+
return this.processBlockCode(block);
|
62
|
+
});
|
63
|
+
await Promise.all(promises);
|
58
64
|
this.out.end();
|
59
65
|
}
|
60
66
|
getStream() {
|
@@ -118,7 +124,7 @@ class StormCodegen {
|
|
118
124
|
}
|
119
125
|
const allFiles = this.toStormFiles(generatedResult);
|
120
126
|
// Send all the non-ai files to the stream
|
121
|
-
this.emitFiles(block.uri, block.aiName, allFiles);
|
127
|
+
this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
|
122
128
|
const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
|
123
129
|
const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
|
124
130
|
if (uiTemplates.length > 0) {
|
@@ -130,7 +136,7 @@ class StormCodegen {
|
|
130
136
|
prompt: this.userPrompt,
|
131
137
|
});
|
132
138
|
uiStream.on('data', (evt) => {
|
133
|
-
this.handleUiOutput(block.uri, block.aiName, evt);
|
139
|
+
this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
|
134
140
|
});
|
135
141
|
await uiStream.waitForDone();
|
136
142
|
}
|
@@ -139,7 +145,7 @@ class StormCodegen {
|
|
139
145
|
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
140
146
|
const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
|
141
147
|
if (serviceFiles.length > 0) {
|
142
|
-
await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
148
|
+
await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
|
143
149
|
}
|
144
150
|
const basePath = this.getBasePath(block.content.metadata.name);
|
145
151
|
for (const serviceFile of serviceFiles) {
|
@@ -154,6 +160,103 @@ class StormCodegen {
|
|
154
160
|
const filePath = (0, path_1.join)(basePath, uiFile.filename);
|
155
161
|
await (0, promises_1.writeFile)(filePath, uiFile.content);
|
156
162
|
}
|
163
|
+
const filesToBeFixed = serviceFiles.concat(contextFiles);
|
164
|
+
const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
|
165
|
+
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
166
|
+
const blockRef = block.uri;
|
167
|
+
this.out.emit('data', {
|
168
|
+
type: 'BLOCK_READY',
|
169
|
+
reason: 'Block ready',
|
170
|
+
created: Date.now(),
|
171
|
+
payload: {
|
172
|
+
path: basePath,
|
173
|
+
blockName: block.aiName,
|
174
|
+
blockRef,
|
175
|
+
instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef),
|
176
|
+
},
|
177
|
+
});
|
178
|
+
}
|
179
|
+
async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
|
180
|
+
let attempts = 0;
|
181
|
+
let validCode = false;
|
182
|
+
for (let i = 0; i <= 3; i++) {
|
183
|
+
attempts++;
|
184
|
+
try {
|
185
|
+
console.log(`Validating the code in ${basePath} attempt #${attempts}`);
|
186
|
+
const result = await codeGenerator.validateForTarget(basePath);
|
187
|
+
if (result && result.valid) {
|
188
|
+
validCode = true;
|
189
|
+
break;
|
190
|
+
}
|
191
|
+
if (result && !result.valid) {
|
192
|
+
console.debug('Validation error:', result);
|
193
|
+
const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
|
194
|
+
const fixes = new Map();
|
195
|
+
errorStream.on('data', (evt) => {
|
196
|
+
if (evt.type === 'ERROR_CLASSIFIER') {
|
197
|
+
// find the file that caused the error
|
198
|
+
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
199
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
200
|
+
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
201
|
+
if (!file) {
|
202
|
+
console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
|
203
|
+
}
|
204
|
+
// read the content of the file
|
205
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
|
206
|
+
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
207
|
+
.map((e) => e.filename)
|
208
|
+
.join('\n')}\n---\n${content}`;
|
209
|
+
console.log(`trying to fix the code in ${eventFileName}`);
|
210
|
+
console.debug(`with the fix:\n${fix}`);
|
211
|
+
const code = this.codeFix(fix);
|
212
|
+
fixes.set((0, path_1.join)(basePath, eventFileName), code);
|
213
|
+
}
|
214
|
+
});
|
215
|
+
await errorStream.waitForDone();
|
216
|
+
for (const [filename, codePromise] of fixes) {
|
217
|
+
const code = await codePromise;
|
218
|
+
(0, fs_1.writeFileSync)(filename, code);
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
catch (e) {
|
223
|
+
console.error('Error:', e);
|
224
|
+
}
|
225
|
+
}
|
226
|
+
if (validCode) {
|
227
|
+
console.log(`Validation successful after ${attempts} attempts`);
|
228
|
+
}
|
229
|
+
else {
|
230
|
+
console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
removePrefix(prefix, str) {
|
234
|
+
if (str.startsWith(prefix)) {
|
235
|
+
return str.slice(prefix.length);
|
236
|
+
}
|
237
|
+
return str;
|
238
|
+
}
|
239
|
+
async writeToFile(fileName, code) {
|
240
|
+
console.log(`writing the fixed code to ${fileName}`);
|
241
|
+
const resolvedCode = await code;
|
242
|
+
(0, fs_1.writeFileSync)(fileName, resolvedCode);
|
243
|
+
}
|
244
|
+
/**
|
245
|
+
* Sends the code to the AI for a fix
|
246
|
+
*/
|
247
|
+
async codeFix(fix) {
|
248
|
+
return new Promise(async (resolve, reject) => {
|
249
|
+
const fixStream = await stormClient_1.stormClient.createCodeFix(fix, []);
|
250
|
+
fixStream.on('data', (evt) => {
|
251
|
+
if (evt.type === 'CODE_FIX') {
|
252
|
+
resolve(evt.payload.content);
|
253
|
+
}
|
254
|
+
});
|
255
|
+
fixStream.on('error', (err) => {
|
256
|
+
reject(err);
|
257
|
+
});
|
258
|
+
await fixStream.waitForDone();
|
259
|
+
});
|
157
260
|
}
|
158
261
|
/**
|
159
262
|
* Emits the text-based files to the stream
|
@@ -6,7 +6,7 @@ import { StormEvent } from './events';
|
|
6
6
|
import { BlockDefinition, Plan } from '@kapeta/schemas';
|
7
7
|
import { KapetaURI } from '@kapeta/nodejs-utils';
|
8
8
|
export interface BlockDefinitionInfo {
|
9
|
-
uri:
|
9
|
+
uri: string;
|
10
10
|
content: BlockDefinition;
|
11
11
|
aiName: string;
|
12
12
|
}
|
@@ -315,6 +315,8 @@ class StormEventParser {
|
|
315
315
|
name: planRef.fullName,
|
316
316
|
title: this.planName,
|
317
317
|
description: this.planDescription,
|
318
|
+
structure: 'mono',
|
319
|
+
visibility: 'private',
|
318
320
|
},
|
319
321
|
spec: {
|
320
322
|
blocks,
|
@@ -331,7 +333,7 @@ class StormEventParser {
|
|
331
333
|
Object.entries(this.blocks).forEach(([, blockInfo]) => {
|
332
334
|
const blockRef = StormEventParser.toRef(handle, blockInfo.name);
|
333
335
|
const blockDefinitionInfo = {
|
334
|
-
uri: blockRef,
|
336
|
+
uri: blockRef.toNormalizedString(),
|
335
337
|
aiName: blockInfo.name,
|
336
338
|
content: {
|
337
339
|
kind: this.toBlockKind(blockInfo.type),
|
@@ -100,6 +100,26 @@ export interface StormEventError {
|
|
100
100
|
error: string;
|
101
101
|
};
|
102
102
|
}
|
103
|
+
export interface StormEventErrorClassifier {
|
104
|
+
type: 'ERROR_CLASSIFIER';
|
105
|
+
reason: string;
|
106
|
+
created: number;
|
107
|
+
payload: StormEventErrorClassifierInfo;
|
108
|
+
}
|
109
|
+
export interface StormEventCodeFix {
|
110
|
+
type: 'CODE_FIX';
|
111
|
+
reason: string;
|
112
|
+
created: number;
|
113
|
+
payload: {
|
114
|
+
filename: string;
|
115
|
+
content: string;
|
116
|
+
};
|
117
|
+
}
|
118
|
+
export interface StormEventErrorClassifierInfo {
|
119
|
+
error: string;
|
120
|
+
filename: string;
|
121
|
+
potentialFix: string;
|
122
|
+
}
|
103
123
|
export interface ScreenTemplate {
|
104
124
|
name: string;
|
105
125
|
template: string;
|
@@ -143,6 +163,17 @@ export interface StormEventFile {
|
|
143
163
|
instanceId: string;
|
144
164
|
};
|
145
165
|
}
|
166
|
+
export interface StormEventBlockReady {
|
167
|
+
type: 'BLOCK_READY';
|
168
|
+
reason: string;
|
169
|
+
created: number;
|
170
|
+
payload: {
|
171
|
+
path: string;
|
172
|
+
blockName: string;
|
173
|
+
blockRef: string;
|
174
|
+
instanceId: string;
|
175
|
+
};
|
176
|
+
}
|
146
177
|
export interface StormEventDone {
|
147
178
|
type: 'DONE';
|
148
179
|
created: number;
|
@@ -153,4 +184,4 @@ export interface StormEventDefinitionChange {
|
|
153
184
|
created: number;
|
154
185
|
payload: StormDefinitions;
|
155
186
|
}
|
156
|
-
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange;
|
187
|
+
export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady;
|
@@ -8,11 +8,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
8
|
};
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
10
|
const express_promise_router_1 = __importDefault(require("express-promise-router"));
|
11
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
11
12
|
const cors_1 = require("../middleware/cors");
|
12
13
|
const stringBody_1 = require("../middleware/stringBody");
|
13
14
|
const stormClient_1 = require("./stormClient");
|
14
15
|
const event_parser_1 = require("./event-parser");
|
15
16
|
const codegen_1 = require("./codegen");
|
17
|
+
const assetManager_1 = require("../assetManager");
|
18
|
+
const path_1 = __importDefault(require("path"));
|
16
19
|
const router = (0, express_promise_router_1.default)();
|
17
20
|
router.use('/', cors_1.corsHandler);
|
18
21
|
router.use('/', stringBody_1.stringBody);
|
@@ -47,7 +50,7 @@ router.post('/:handle/all', async (req, res) => {
|
|
47
50
|
const result = eventParser.toResult(handle);
|
48
51
|
sendDefinitions(res, result);
|
49
52
|
if (!req.query.skipCodegen) {
|
50
|
-
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
|
53
|
+
const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
|
51
54
|
const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
|
52
55
|
await stormCodegen.process();
|
53
56
|
await codegenPromise;
|
@@ -59,6 +62,28 @@ router.post('/:handle/all', async (req, res) => {
|
|
59
62
|
res.end();
|
60
63
|
}
|
61
64
|
});
|
65
|
+
router.post('/block/create', async (req, res) => {
|
66
|
+
const createRequest = JSON.parse(req.stringBody ?? '{}');
|
67
|
+
try {
|
68
|
+
const ymlPath = path_1.default.join(createRequest.newPath, 'kapeta.yml');
|
69
|
+
console.log('Creating block at', ymlPath);
|
70
|
+
const [asset] = await assetManager_1.assetManager.createAsset(ymlPath, createRequest.definition);
|
71
|
+
if (await fs_extra_1.default.pathExists(createRequest.tmpPath)) {
|
72
|
+
console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
|
73
|
+
await fs_extra_1.default.move(createRequest.tmpPath, createRequest.newPath, {
|
74
|
+
overwrite: true,
|
75
|
+
});
|
76
|
+
console.log('Updating asset', asset.ref);
|
77
|
+
res.send(await assetManager_1.assetManager.updateAsset(asset.ref, createRequest.definition));
|
78
|
+
}
|
79
|
+
else {
|
80
|
+
res.send(asset);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
catch (err) {
|
84
|
+
res.status(500).send({ error: err.message });
|
85
|
+
}
|
86
|
+
});
|
62
87
|
function sendDefinitions(res, result) {
|
63
88
|
sendEvent(res, {
|
64
89
|
type: 'DEFINITION_CHANGE',
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
|
1
|
+
import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
|
2
2
|
export declare const STORM_ID = "storm";
|
3
3
|
export declare const ConversationIdHeader = "Conversation-Id";
|
4
4
|
declare class StormClient {
|
@@ -9,6 +9,8 @@ declare class StormClient {
|
|
9
9
|
createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
|
10
10
|
createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
|
11
11
|
createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
|
12
|
+
createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
13
|
+
createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
|
12
14
|
}
|
13
15
|
export declare const stormClient: StormClient;
|
14
16
|
export {};
|
@@ -83,5 +83,17 @@ class StormClient {
|
|
83
83
|
conversationId,
|
84
84
|
});
|
85
85
|
}
|
86
|
+
createErrorClassification(prompt, history, conversationId) {
|
87
|
+
return this.send('/v2/code/errorclassifier', {
|
88
|
+
conversationId: conversationId,
|
89
|
+
prompt,
|
90
|
+
});
|
91
|
+
}
|
92
|
+
createCodeFix(prompt, history, conversationId) {
|
93
|
+
return this.send('/v2/code/fix', {
|
94
|
+
conversationId: conversationId,
|
95
|
+
prompt,
|
96
|
+
});
|
97
|
+
}
|
86
98
|
}
|
87
99
|
exports.stormClient = new StormClient();
|
@@ -6,6 +6,7 @@
|
|
6
6
|
import { EventEmitter } from 'node:events';
|
7
7
|
import { StormEvent } from './events';
|
8
8
|
import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
|
9
|
+
import { BlockDefinition } from '@kapeta/schemas';
|
9
10
|
export declare class StormStream extends EventEmitter {
|
10
11
|
private conversationId;
|
11
12
|
private lines;
|
@@ -29,6 +30,11 @@ export interface StormContextRequest<T = string> {
|
|
29
30
|
conversationId?: string;
|
30
31
|
prompt: T;
|
31
32
|
}
|
33
|
+
export interface StormCreateBlockRequest {
|
34
|
+
definition: BlockDefinition;
|
35
|
+
tmpPath: string;
|
36
|
+
newPath: string;
|
37
|
+
}
|
32
38
|
export interface StormFileInfo extends GeneratedFile {
|
33
39
|
type: AIFileTypes;
|
34
40
|
}
|
package/package.json
CHANGED
package/src/assetManager.ts
CHANGED
@@ -179,7 +179,8 @@ class AssetManager {
|
|
179
179
|
async createAsset(
|
180
180
|
path: string,
|
181
181
|
yaml: BlockDefinition,
|
182
|
-
sourceOfChange: SourceOfChange = 'filesystem'
|
182
|
+
sourceOfChange: SourceOfChange = 'filesystem',
|
183
|
+
codegen: boolean = true
|
183
184
|
): Promise<EnrichedAsset[]> {
|
184
185
|
if (await FS.pathExists(path)) {
|
185
186
|
throw new Error('File already exists: ' + path);
|
@@ -199,11 +200,12 @@ class AssetManager {
|
|
199
200
|
});
|
200
201
|
|
201
202
|
definitionsManager.clearCache();
|
202
|
-
console.log(`Created asset at: ${path}`);
|
203
|
-
|
204
203
|
const ref = `kapeta://${yaml.metadata.name}:local`;
|
204
|
+
console.log(`Created asset ${ref} at: ${path}`);
|
205
205
|
|
206
|
-
|
206
|
+
if (codegen) {
|
207
|
+
await this.maybeGenerateCode(ref, path, yaml);
|
208
|
+
}
|
207
209
|
|
208
210
|
return asset;
|
209
211
|
}
|
package/src/filesystem/routes.ts
CHANGED
@@ -8,6 +8,7 @@ import { stringBody, StringBodyRequest } from '../middleware/stringBody';
|
|
8
8
|
import { filesystemManager } from '../filesystemManager';
|
9
9
|
import { corsHandler } from '../middleware/cors';
|
10
10
|
import { NextFunction, Request, Response } from 'express';
|
11
|
+
import FS from 'fs-extra';
|
11
12
|
|
12
13
|
let router = Router();
|
13
14
|
|
@@ -99,6 +100,15 @@ router.get('/readfile', async (req: Request, res: Response) => {
|
|
99
100
|
}
|
100
101
|
});
|
101
102
|
|
103
|
+
router.get('/exists', async (req: Request, res: Response) => {
|
104
|
+
let pathArg = req.query.path as string;
|
105
|
+
try {
|
106
|
+
res.send(await FS.pathExists(pathArg));
|
107
|
+
} catch (err) {
|
108
|
+
res.status(400).send({ error: '' + err });
|
109
|
+
}
|
110
|
+
});
|
111
|
+
|
102
112
|
router.put('/mkdir', async (req: Request, res: Response) => {
|
103
113
|
let pathArg = req.query.path as string;
|
104
114
|
try {
|
package/src/storm/codegen.ts
CHANGED
@@ -4,17 +4,26 @@
|
|
4
4
|
*/
|
5
5
|
|
6
6
|
import { Definition } from '@kapeta/local-cluster-config';
|
7
|
-
import {
|
7
|
+
import {
|
8
|
+
AIFileTypes,
|
9
|
+
BlockCodeGenerator,
|
10
|
+
CodeGenerator,
|
11
|
+
CodeWriter,
|
12
|
+
GeneratedFile,
|
13
|
+
GeneratedResult,
|
14
|
+
} from '@kapeta/codegen';
|
8
15
|
import { BlockDefinition } from '@kapeta/schemas';
|
9
16
|
import { codeGeneratorManager } from '../codeGeneratorManager';
|
10
17
|
import { STORM_ID, stormClient } from './stormClient';
|
11
18
|
import { StormEvent, StormEventFile } from './events';
|
12
19
|
import { BlockDefinitionInfo, StormEventParser } from './event-parser';
|
13
|
-
import {
|
14
|
-
import { KapetaURI } from '@kapeta/nodejs-utils';
|
20
|
+
import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
|
21
|
+
import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
|
15
22
|
import { writeFile } from 'fs/promises';
|
16
23
|
import path, { join } from 'path';
|
17
24
|
import os from 'node:os';
|
25
|
+
import { readFile, readFileSync, writeFileSync } from 'fs';
|
26
|
+
import Path from 'path';
|
18
27
|
|
19
28
|
type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
|
20
29
|
|
@@ -24,19 +33,21 @@ export class StormCodegen {
|
|
24
33
|
private readonly out = new StormStream();
|
25
34
|
private readonly events: StormEvent[];
|
26
35
|
private readonly tmpDir: string;
|
36
|
+
private readonly conversationId: string;
|
27
37
|
|
28
|
-
constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
38
|
+
constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
|
29
39
|
this.userPrompt = userPrompt;
|
30
40
|
this.blocks = blocks;
|
31
41
|
this.events = events;
|
32
|
-
this.tmpDir = os.tmpdir();
|
42
|
+
this.tmpDir = Path.join(os.tmpdir(), conversationId);
|
43
|
+
this.conversationId = conversationId;
|
33
44
|
}
|
34
45
|
|
35
46
|
public async process() {
|
36
|
-
|
37
|
-
|
38
|
-
}
|
39
|
-
|
47
|
+
const promises = this.blocks.map((block) => {
|
48
|
+
return this.processBlockCode(block);
|
49
|
+
});
|
50
|
+
await Promise.all(promises);
|
40
51
|
this.out.end();
|
41
52
|
}
|
42
53
|
|
@@ -108,7 +119,7 @@ export class StormCodegen {
|
|
108
119
|
const allFiles = this.toStormFiles(generatedResult);
|
109
120
|
|
110
121
|
// Send all the non-ai files to the stream
|
111
|
-
this.emitFiles(block.uri, block.aiName, allFiles);
|
122
|
+
this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
|
112
123
|
|
113
124
|
const relevantFiles: StormFileInfo[] = allFiles.filter(
|
114
125
|
(file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
|
@@ -124,7 +135,7 @@ export class StormCodegen {
|
|
124
135
|
});
|
125
136
|
|
126
137
|
uiStream.on('data', (evt) => {
|
127
|
-
this.handleUiOutput(block.uri, block.aiName, evt);
|
138
|
+
this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
|
128
139
|
});
|
129
140
|
|
130
141
|
await uiStream.waitForDone();
|
@@ -139,7 +150,7 @@ export class StormCodegen {
|
|
139
150
|
const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
|
140
151
|
if (serviceFiles.length > 0) {
|
141
152
|
await this.processTemplates(
|
142
|
-
block.uri,
|
153
|
+
parseKapetaUri(block.uri),
|
143
154
|
block.aiName,
|
144
155
|
stormClient.createServiceImplementation.bind(stormClient),
|
145
156
|
serviceFiles,
|
@@ -163,6 +174,115 @@ export class StormCodegen {
|
|
163
174
|
const filePath = join(basePath, uiFile.filename);
|
164
175
|
await writeFile(filePath, uiFile.content);
|
165
176
|
}
|
177
|
+
|
178
|
+
const filesToBeFixed = serviceFiles.concat(contextFiles);
|
179
|
+
const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
|
180
|
+
await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
|
181
|
+
|
182
|
+
const blockRef = block.uri;
|
183
|
+
this.out.emit('data', {
|
184
|
+
type: 'BLOCK_READY',
|
185
|
+
reason: 'Block ready',
|
186
|
+
created: Date.now(),
|
187
|
+
payload: {
|
188
|
+
path: basePath,
|
189
|
+
blockName: block.aiName,
|
190
|
+
blockRef,
|
191
|
+
instanceId: StormEventParser.toInstanceIdFromRef(blockRef),
|
192
|
+
},
|
193
|
+
} satisfies StormEvent);
|
194
|
+
}
|
195
|
+
|
196
|
+
private async verifyAndFixCode(
|
197
|
+
codeGenerator: CodeGenerator,
|
198
|
+
basePath: string,
|
199
|
+
filesToBeFixed: StormFileInfo[],
|
200
|
+
knownFiles: StormFileInfo[]
|
201
|
+
) {
|
202
|
+
let attempts = 0;
|
203
|
+
let validCode = false;
|
204
|
+
for (let i = 0; i <= 3; i++) {
|
205
|
+
attempts++;
|
206
|
+
try {
|
207
|
+
console.log(`Validating the code in ${basePath} attempt #${attempts}`);
|
208
|
+
const result = await codeGenerator.validateForTarget(basePath);
|
209
|
+
if (result && result.valid) {
|
210
|
+
validCode = true;
|
211
|
+
break;
|
212
|
+
}
|
213
|
+
|
214
|
+
if (result && !result.valid) {
|
215
|
+
console.debug('Validation error:', result);
|
216
|
+
const errorStream = await stormClient.createErrorClassification(result.error, []);
|
217
|
+
const fixes = new Map<string, Promise<string>>();
|
218
|
+
|
219
|
+
errorStream.on('data', (evt) => {
|
220
|
+
if (evt.type === 'ERROR_CLASSIFIER') {
|
221
|
+
// find the file that caused the error
|
222
|
+
// strip base path from event file name, if it exists sometimes the AI sends the full path
|
223
|
+
const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
|
224
|
+
const file = filesToBeFixed.find((f) => f.filename === eventFileName);
|
225
|
+
if (!file) {
|
226
|
+
console.log(
|
227
|
+
`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
|
228
|
+
);
|
229
|
+
}
|
230
|
+
// read the content of the file
|
231
|
+
const content = readFileSync(join(basePath, eventFileName), 'utf8');
|
232
|
+
const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
|
233
|
+
.map((e) => e.filename)
|
234
|
+
.join('\n')}\n---\n${content}`;
|
235
|
+
console.log(`trying to fix the code in ${eventFileName}`);
|
236
|
+
console.debug(`with the fix:\n${fix}`);
|
237
|
+
const code = this.codeFix(fix);
|
238
|
+
fixes.set(join(basePath, eventFileName), code);
|
239
|
+
}
|
240
|
+
});
|
241
|
+
|
242
|
+
await errorStream.waitForDone();
|
243
|
+
for (const [filename, codePromise] of fixes) {
|
244
|
+
const code = await codePromise;
|
245
|
+
writeFileSync(filename, code);
|
246
|
+
}
|
247
|
+
}
|
248
|
+
} catch (e) {
|
249
|
+
console.error('Error:', e);
|
250
|
+
}
|
251
|
+
}
|
252
|
+
if (validCode) {
|
253
|
+
console.log(`Validation successful after ${attempts} attempts`);
|
254
|
+
} else {
|
255
|
+
console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
removePrefix(prefix: string, str: string): string {
|
260
|
+
if (str.startsWith(prefix)) {
|
261
|
+
return str.slice(prefix.length);
|
262
|
+
}
|
263
|
+
return str;
|
264
|
+
}
|
265
|
+
async writeToFile(fileName: string, code: Promise<string>) {
|
266
|
+
console.log(`writing the fixed code to ${fileName}`);
|
267
|
+
const resolvedCode = await code;
|
268
|
+
writeFileSync(fileName, resolvedCode);
|
269
|
+
}
|
270
|
+
/**
|
271
|
+
* Sends the code to the AI for a fix
|
272
|
+
*/
|
273
|
+
private async codeFix(fix: string): Promise<string> {
|
274
|
+
return new Promise<string>(async (resolve, reject) => {
|
275
|
+
const fixStream = await stormClient.createCodeFix(fix, []);
|
276
|
+
fixStream.on('data', (evt) => {
|
277
|
+
if (evt.type === 'CODE_FIX') {
|
278
|
+
resolve(evt.payload.content);
|
279
|
+
}
|
280
|
+
});
|
281
|
+
fixStream.on('error', (err) => {
|
282
|
+
reject(err);
|
283
|
+
});
|
284
|
+
await fixStream.waitForDone();
|
285
|
+
});
|
166
286
|
}
|
167
287
|
|
168
288
|
/**
|
@@ -29,7 +29,7 @@ import { v5 as uuid } from 'uuid';
|
|
29
29
|
import { definitionsManager } from '../definitionsManager';
|
30
30
|
|
31
31
|
export interface BlockDefinitionInfo {
|
32
|
-
uri:
|
32
|
+
uri: string;
|
33
33
|
content: BlockDefinition;
|
34
34
|
aiName: string;
|
35
35
|
}
|
@@ -454,6 +454,8 @@ export class StormEventParser {
|
|
454
454
|
name: planRef.fullName,
|
455
455
|
title: this.planName,
|
456
456
|
description: this.planDescription,
|
457
|
+
structure: 'mono',
|
458
|
+
visibility: 'private',
|
457
459
|
},
|
458
460
|
spec: {
|
459
461
|
blocks,
|
@@ -474,7 +476,7 @@ export class StormEventParser {
|
|
474
476
|
const blockRef = StormEventParser.toRef(handle, blockInfo.name);
|
475
477
|
|
476
478
|
const blockDefinitionInfo: BlockDefinitionInfo = {
|
477
|
-
uri: blockRef,
|
479
|
+
uri: blockRef.toNormalizedString(),
|
478
480
|
aiName: blockInfo.name,
|
479
481
|
content: {
|
480
482
|
kind: this.toBlockKind(blockInfo.type),
|