@kapeta/local-cluster-service 0.44.0 → 0.45.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 +7 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/src/codeGeneratorManager.d.ts +1 -0
- package/dist/cjs/src/codeGeneratorManager.js +12 -6
- package/dist/cjs/src/middleware/cors.d.ts +1 -0
- package/dist/cjs/src/middleware/kapeta.d.ts +1 -0
- package/dist/cjs/src/middleware/stringBody.d.ts +1 -0
- package/dist/cjs/src/storm/codegen.d.ts +36 -0
- package/dist/cjs/src/storm/codegen.js +160 -0
- package/dist/cjs/src/storm/event-parser.d.ts +70 -0
- package/dist/cjs/src/storm/event-parser.js +543 -0
- package/dist/cjs/src/storm/events.d.ts +127 -0
- package/dist/cjs/src/storm/events.js +6 -0
- package/dist/cjs/src/storm/routes.d.ts +7 -0
- package/dist/cjs/src/storm/routes.js +109 -0
- package/dist/cjs/src/storm/stormClient.d.ts +13 -0
- package/dist/cjs/src/storm/stormClient.js +87 -0
- package/dist/cjs/src/storm/stream.d.ts +38 -0
- package/dist/cjs/src/storm/stream.js +57 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/src/codeGeneratorManager.d.ts +1 -0
- package/dist/esm/src/codeGeneratorManager.js +12 -6
- package/dist/esm/src/middleware/cors.d.ts +1 -0
- package/dist/esm/src/middleware/kapeta.d.ts +1 -0
- package/dist/esm/src/middleware/stringBody.d.ts +1 -0
- package/dist/esm/src/storm/codegen.d.ts +36 -0
- package/dist/esm/src/storm/codegen.js +160 -0
- package/dist/esm/src/storm/event-parser.d.ts +70 -0
- package/dist/esm/src/storm/event-parser.js +543 -0
- package/dist/esm/src/storm/events.d.ts +127 -0
- package/dist/esm/src/storm/events.js +6 -0
- package/dist/esm/src/storm/routes.d.ts +7 -0
- package/dist/esm/src/storm/routes.js +109 -0
- package/dist/esm/src/storm/stormClient.d.ts +13 -0
- package/dist/esm/src/storm/stormClient.js +87 -0
- package/dist/esm/src/storm/stream.d.ts +38 -0
- package/dist/esm/src/storm/stream.js +57 -0
- package/index.ts +2 -0
- package/package.json +3 -3
- package/src/codeGeneratorManager.ts +17 -8
- package/src/storm/codegen.ts +210 -0
- package/src/storm/event-parser.ts +688 -0
- package/src/storm/events.ts +169 -0
- package/src/storm/routes.ts +143 -0
- package/src/storm/stormClient.ts +114 -0
- package/src/storm/stream.ts +88 -0
- package/src/utils/BlockInstanceRunner.ts +4 -2
@@ -0,0 +1,109 @@
|
|
1
|
+
"use strict";
|
2
|
+
/**
|
3
|
+
* Copyright 2023 Kapeta Inc.
|
4
|
+
* SPDX-License-Identifier: BUSL-1.1
|
5
|
+
*/
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
|
+
};
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
10
|
+
const express_promise_router_1 = __importDefault(require("express-promise-router"));
|
11
|
+
const cors_1 = require("../middleware/cors");
|
12
|
+
const stringBody_1 = require("../middleware/stringBody");
|
13
|
+
const stormClient_1 = require("./stormClient");
|
14
|
+
const event_parser_1 = require("./event-parser");
|
15
|
+
const codegen_1 = require("./codegen");
|
16
|
+
const router = (0, express_promise_router_1.default)();
|
17
|
+
router.use('/', cors_1.corsHandler);
|
18
|
+
router.use('/', stringBody_1.stringBody);
|
19
|
+
router.post('/:handle/all', async (req, res) => {
|
20
|
+
const handle = req.params.handle;
|
21
|
+
try {
|
22
|
+
const stormOptions = await (0, event_parser_1.resolveOptions)();
|
23
|
+
const eventParser = new event_parser_1.StormEventParser(stormOptions);
|
24
|
+
console.log('Got prompt', req.stringBody);
|
25
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
26
|
+
const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
27
|
+
res.set('Content-Type', 'application/x-ndjson');
|
28
|
+
metaStream.on('data', (data) => {
|
29
|
+
eventParser.addEvent(data);
|
30
|
+
});
|
31
|
+
await streamStormPartialResponse(metaStream, res);
|
32
|
+
if (!eventParser.isValid()) {
|
33
|
+
// We can't continue if the meta stream is invalid
|
34
|
+
res.write({
|
35
|
+
type: 'ERROR_INTERNAL',
|
36
|
+
payload: { error: eventParser.getError() },
|
37
|
+
reason: 'Failed to generate system',
|
38
|
+
created: Date.now(),
|
39
|
+
});
|
40
|
+
res.end();
|
41
|
+
return;
|
42
|
+
}
|
43
|
+
const result = eventParser.toResult(handle);
|
44
|
+
console.log('RESULT\n', JSON.stringify(result, null, 2));
|
45
|
+
const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks);
|
46
|
+
const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
|
47
|
+
await stormCodegen.process();
|
48
|
+
await codegenStream;
|
49
|
+
sendDone(res);
|
50
|
+
}
|
51
|
+
catch (err) {
|
52
|
+
sendError(err, res);
|
53
|
+
}
|
54
|
+
});
|
55
|
+
router.post('/metadata', async (req, res) => {
|
56
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
57
|
+
const result = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
|
58
|
+
await streamStormResponse(result, res);
|
59
|
+
});
|
60
|
+
router.post('/services/implement', async (req, res) => {
|
61
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
62
|
+
const result = await stormClient_1.stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
|
63
|
+
await streamStormResponse(result, res);
|
64
|
+
});
|
65
|
+
router.post('/ui/implement', async (req, res) => {
|
66
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
67
|
+
const result = await stormClient_1.stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
|
68
|
+
await streamStormResponse(result, res);
|
69
|
+
});
|
70
|
+
async function streamStormResponse(result, res) {
|
71
|
+
res.set('Content-Type', 'application/x-ndjson');
|
72
|
+
await streamStormPartialResponse(result, res);
|
73
|
+
sendDone(res);
|
74
|
+
}
|
75
|
+
function sendDone(res) {
|
76
|
+
res.write(JSON.stringify({
|
77
|
+
type: 'DONE',
|
78
|
+
created: Date.now(),
|
79
|
+
}) + '\n');
|
80
|
+
res.end();
|
81
|
+
}
|
82
|
+
function sendError(err, res) {
|
83
|
+
console.error('Failed to send prompt', err);
|
84
|
+
if (res.headersSent) {
|
85
|
+
res.write(JSON.stringify({
|
86
|
+
type: 'ERROR_INTERNAL',
|
87
|
+
created: Date.now(),
|
88
|
+
payload: { error: err.message },
|
89
|
+
reason: 'Failed while sending prompt',
|
90
|
+
}) + '\n');
|
91
|
+
}
|
92
|
+
else {
|
93
|
+
res.status(400).send({ error: err.message });
|
94
|
+
}
|
95
|
+
}
|
96
|
+
function streamStormPartialResponse(result, res) {
|
97
|
+
return new Promise((resolve, reject) => {
|
98
|
+
result.on('data', (data) => {
|
99
|
+
res.write(JSON.stringify(data) + '\n');
|
100
|
+
});
|
101
|
+
result.on('error', (err) => {
|
102
|
+
reject(err);
|
103
|
+
});
|
104
|
+
result.on('end', () => {
|
105
|
+
resolve();
|
106
|
+
});
|
107
|
+
});
|
108
|
+
}
|
109
|
+
exports.default = router;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { ConversationItem, StormFileImplementationPrompt, StormStream } from './stream';
|
2
|
+
export declare const STORM_ID = "storm";
|
3
|
+
declare class StormClient {
|
4
|
+
private readonly _baseUrl;
|
5
|
+
constructor();
|
6
|
+
private createOptions;
|
7
|
+
private send;
|
8
|
+
createMetadata(prompt: string, history?: ConversationItem[]): Promise<StormStream>;
|
9
|
+
createUIImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
10
|
+
createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]): Promise<StormStream>;
|
11
|
+
}
|
12
|
+
export declare const stormClient: StormClient;
|
13
|
+
export {};
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.stormClient = exports.STORM_ID = void 0;
|
7
|
+
/**
|
8
|
+
* Copyright 2023 Kapeta Inc.
|
9
|
+
* SPDX-License-Identifier: BUSL-1.1
|
10
|
+
*/
|
11
|
+
const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
|
12
|
+
const utils_1 = require("../utils/utils");
|
13
|
+
const promises_1 = __importDefault(require("node:readline/promises"));
|
14
|
+
const node_stream_1 = require("node:stream");
|
15
|
+
const stream_1 = require("./stream");
|
16
|
+
exports.STORM_ID = 'storm';
|
17
|
+
class StormClient {
|
18
|
+
_baseUrl;
|
19
|
+
constructor() {
|
20
|
+
this._baseUrl = (0, utils_1.getRemoteUrl)('ai-service', 'https://ai.kapeta.com');
|
21
|
+
}
|
22
|
+
createOptions(path, method, body) {
|
23
|
+
const url = `${this._baseUrl}${path}`;
|
24
|
+
const headers = {
|
25
|
+
'Content-Type': 'application/json',
|
26
|
+
};
|
27
|
+
const api = new nodejs_api_client_1.KapetaAPI();
|
28
|
+
if (api.hasToken()) {
|
29
|
+
//headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
|
30
|
+
}
|
31
|
+
return {
|
32
|
+
url,
|
33
|
+
method: method,
|
34
|
+
body: JSON.stringify(body),
|
35
|
+
headers,
|
36
|
+
};
|
37
|
+
}
|
38
|
+
async send(path, body, method = 'POST') {
|
39
|
+
const stringPrompt = typeof body.prompt === 'string' ? body.prompt : JSON.stringify(body.prompt);
|
40
|
+
const options = this.createOptions(path, method, {
|
41
|
+
prompt: stringPrompt,
|
42
|
+
history: body.history,
|
43
|
+
});
|
44
|
+
const out = new stream_1.StormStream(stringPrompt, body.history);
|
45
|
+
const response = await fetch(options.url, options);
|
46
|
+
if (response.status !== 200) {
|
47
|
+
throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
|
48
|
+
}
|
49
|
+
const jsonLStream = promises_1.default.createInterface(node_stream_1.Readable.fromWeb(response.body));
|
50
|
+
jsonLStream.on('line', (line) => {
|
51
|
+
out.addJSONLine(line);
|
52
|
+
});
|
53
|
+
jsonLStream.on('error', (error) => {
|
54
|
+
out.emit('error', error);
|
55
|
+
});
|
56
|
+
jsonLStream.on('pause', () => {
|
57
|
+
console.log('paused');
|
58
|
+
});
|
59
|
+
jsonLStream.on('resume', () => {
|
60
|
+
console.log('resumed');
|
61
|
+
});
|
62
|
+
jsonLStream.on('close', () => {
|
63
|
+
out.end();
|
64
|
+
});
|
65
|
+
return out;
|
66
|
+
}
|
67
|
+
createMetadata(prompt, history) {
|
68
|
+
return this.send('/v2/all', {
|
69
|
+
history: history ?? [],
|
70
|
+
prompt: prompt,
|
71
|
+
});
|
72
|
+
}
|
73
|
+
createUIImplementation(prompt, history) {
|
74
|
+
return this.send('/v2/ui/merge', {
|
75
|
+
history: history ?? [],
|
76
|
+
prompt,
|
77
|
+
});
|
78
|
+
}
|
79
|
+
createServiceImplementation(prompt, history) {
|
80
|
+
console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
|
81
|
+
return this.send('/v2/services/merge', {
|
82
|
+
history: history ?? [],
|
83
|
+
prompt,
|
84
|
+
});
|
85
|
+
}
|
86
|
+
}
|
87
|
+
exports.stormClient = new StormClient();
|
@@ -0,0 +1,38 @@
|
|
1
|
+
/// <reference types="node" />
|
2
|
+
/**
|
3
|
+
* Copyright 2023 Kapeta Inc.
|
4
|
+
* SPDX-License-Identifier: BUSL-1.1
|
5
|
+
*/
|
6
|
+
import { EventEmitter } from 'node:events';
|
7
|
+
import { StormEvent } from './events';
|
8
|
+
import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
|
9
|
+
export declare class StormStream extends EventEmitter {
|
10
|
+
private history;
|
11
|
+
private lines;
|
12
|
+
constructor(prompt?: string, history?: ConversationItem[]);
|
13
|
+
addJSONLine(line: string): void;
|
14
|
+
end(): void;
|
15
|
+
on(event: 'end', listener: () => void): this;
|
16
|
+
on(event: 'error', listener: (e: Error) => void): this;
|
17
|
+
on(event: 'data', listener: (data: StormEvent) => void): this;
|
18
|
+
emit(event: 'end'): boolean;
|
19
|
+
emit(event: 'error', e: Error): boolean;
|
20
|
+
emit(event: 'data', data: StormEvent): boolean;
|
21
|
+
waitForDone(): Promise<void>;
|
22
|
+
}
|
23
|
+
export interface ConversationItem {
|
24
|
+
role: 'user' | 'model';
|
25
|
+
content: string;
|
26
|
+
}
|
27
|
+
export interface StormContextRequest<T = string> {
|
28
|
+
history?: ConversationItem[];
|
29
|
+
prompt: T;
|
30
|
+
}
|
31
|
+
export interface StormFileInfo extends GeneratedFile {
|
32
|
+
type: AIFileTypes;
|
33
|
+
}
|
34
|
+
export interface StormFileImplementationPrompt {
|
35
|
+
context: StormFileInfo[];
|
36
|
+
template: StormFileInfo;
|
37
|
+
prompt: string;
|
38
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.StormStream = void 0;
|
4
|
+
/**
|
5
|
+
* Copyright 2023 Kapeta Inc.
|
6
|
+
* SPDX-License-Identifier: BUSL-1.1
|
7
|
+
*/
|
8
|
+
const node_events_1 = require("node:events");
|
9
|
+
class StormStream extends node_events_1.EventEmitter {
|
10
|
+
history;
|
11
|
+
lines = [];
|
12
|
+
constructor(prompt = '', history) {
|
13
|
+
super();
|
14
|
+
this.history = history ?? [];
|
15
|
+
this.history.push({
|
16
|
+
role: 'user',
|
17
|
+
content: prompt,
|
18
|
+
});
|
19
|
+
}
|
20
|
+
addJSONLine(line) {
|
21
|
+
try {
|
22
|
+
this.lines.push(line);
|
23
|
+
const event = JSON.parse(line);
|
24
|
+
if (!event.created) {
|
25
|
+
event.created = Date.now();
|
26
|
+
}
|
27
|
+
this.emit('data', event);
|
28
|
+
}
|
29
|
+
catch (e) {
|
30
|
+
this.emit('error', e);
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end() {
|
34
|
+
this.history.push({
|
35
|
+
role: 'model',
|
36
|
+
content: this.lines.join('\n'),
|
37
|
+
});
|
38
|
+
this.emit('end');
|
39
|
+
}
|
40
|
+
on(event, listener) {
|
41
|
+
return super.on(event, listener);
|
42
|
+
}
|
43
|
+
emit(eventName, ...args) {
|
44
|
+
return super.emit(eventName, ...args);
|
45
|
+
}
|
46
|
+
waitForDone() {
|
47
|
+
return new Promise((resolve, reject) => {
|
48
|
+
this.on('error', (err) => {
|
49
|
+
reject(err);
|
50
|
+
});
|
51
|
+
this.on('end', () => {
|
52
|
+
resolve();
|
53
|
+
});
|
54
|
+
});
|
55
|
+
}
|
56
|
+
}
|
57
|
+
exports.StormStream = StormStream;
|
package/index.ts
CHANGED
@@ -24,6 +24,7 @@ import AttachmentRoutes from './src/attachments/routes';
|
|
24
24
|
import TaskRoutes from './src/tasks/routes';
|
25
25
|
import APIRoutes from './src/api';
|
26
26
|
import AIRoutes from './src/ai/routes';
|
27
|
+
import StormRoutes from './src/storm/routes';
|
27
28
|
import { isLinux } from './src/utils/utils';
|
28
29
|
import request from 'request';
|
29
30
|
import { repositoryManager } from './src/repositoryManager';
|
@@ -74,6 +75,7 @@ function createServer() {
|
|
74
75
|
app.use('/tasks', TaskRoutes);
|
75
76
|
app.use('/api', APIRoutes);
|
76
77
|
app.use('/ai', AIRoutes);
|
78
|
+
app.use('/storm', StormRoutes);
|
77
79
|
|
78
80
|
app.get('/status', async (req, res) => {
|
79
81
|
res.send({
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.45.0",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -50,9 +50,9 @@
|
|
50
50
|
},
|
51
51
|
"homepage": "https://github.com/kapetacom/local-cluster-service#readme",
|
52
52
|
"dependencies": {
|
53
|
-
"@kapeta/codegen": "^1.
|
53
|
+
"@kapeta/codegen": "^1.4.0",
|
54
54
|
"@kapeta/config-mapper": "^1.2.1",
|
55
|
-
"@kapeta/kaplang-core": "^1.
|
55
|
+
"@kapeta/kaplang-core": "^1.16.0",
|
56
56
|
"@kapeta/local-cluster-config": "^0.4.2",
|
57
57
|
"@kapeta/nodejs-api-client": ">=0.2.0 <2",
|
58
58
|
"@kapeta/nodejs-process": "^1.2.0",
|
@@ -16,6 +16,22 @@ const TARGET_KIND = 'core/language-target';
|
|
16
16
|
const BLOCK_TYPE_REGEX = /^core\/block-type.*/;
|
17
17
|
|
18
18
|
class CodeGeneratorManager {
|
19
|
+
public async ensureTarget(targetKind: string) {
|
20
|
+
const targetRef = normalizeKapetaUri(targetKind);
|
21
|
+
|
22
|
+
// Automatically downloads target if not available
|
23
|
+
const targetAsset = await assetManager.getAsset(targetRef);
|
24
|
+
|
25
|
+
if (!targetAsset) {
|
26
|
+
console.error('Language target not found: %s', targetKind);
|
27
|
+
return false;
|
28
|
+
}
|
29
|
+
|
30
|
+
await this.ensureLanguageTargetInRegistry(targetAsset?.path, targetAsset?.version, targetAsset?.data);
|
31
|
+
|
32
|
+
return true;
|
33
|
+
}
|
34
|
+
|
19
35
|
private async ensureLanguageTargetInRegistry(path: string, version: string, definition: Definition) {
|
20
36
|
const key = `${definition.metadata.name}:${version}`;
|
21
37
|
|
@@ -81,17 +97,10 @@ class CodeGeneratorManager {
|
|
81
97
|
return;
|
82
98
|
}
|
83
99
|
|
84
|
-
|
85
|
-
|
86
|
-
// Automatically downloads target if not available
|
87
|
-
const targetAsset = await assetManager.getAsset(targetRef);
|
88
|
-
|
89
|
-
if (!targetAsset) {
|
90
|
-
console.error('Language target not found: %s', yamlContent.spec.target?.kind);
|
100
|
+
if (!(await this.ensureTarget(yamlContent.spec.target?.kind))) {
|
91
101
|
return;
|
92
102
|
}
|
93
103
|
|
94
|
-
await this.ensureLanguageTargetInRegistry(targetAsset?.path, targetAsset?.version, targetAsset?.data);
|
95
104
|
const baseDir = Path.dirname(yamlFile);
|
96
105
|
console.log('Generating code for path: %s', baseDir);
|
97
106
|
const codeGenerator = new BlockCodeGenerator(yamlContent as BlockDefinition);
|
@@ -0,0 +1,210 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { Definition } from '@kapeta/local-cluster-config';
|
7
|
+
import { AIFileTypes, BlockCodeGenerator, GeneratedFile, GeneratedResult } from '@kapeta/codegen';
|
8
|
+
import { BlockDefinition } from '@kapeta/schemas';
|
9
|
+
import { codeGeneratorManager } from '../codeGeneratorManager';
|
10
|
+
import { STORM_ID, stormClient } from './stormClient';
|
11
|
+
import { ScreenTemplate, StormEvent, StormEventFile } from './events';
|
12
|
+
import { BlockDefinitionInfo } from './event-parser';
|
13
|
+
import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
|
14
|
+
import { KapetaURI } from '@kapeta/nodejs-utils';
|
15
|
+
|
16
|
+
type ImplementationGenerator = (
|
17
|
+
prompt: StormFileImplementationPrompt,
|
18
|
+
history?: ConversationItem[]
|
19
|
+
) => Promise<StormStream>;
|
20
|
+
|
21
|
+
export class StormCodegen {
|
22
|
+
private readonly userPrompt: string;
|
23
|
+
private readonly blocks: BlockDefinitionInfo[];
|
24
|
+
private readonly out = new StormStream();
|
25
|
+
|
26
|
+
constructor(userPrompt: string, blocks: BlockDefinitionInfo[]) {
|
27
|
+
this.userPrompt = userPrompt;
|
28
|
+
this.blocks = blocks;
|
29
|
+
}
|
30
|
+
|
31
|
+
public async process() {
|
32
|
+
console.log('Processing blocks', this.blocks.length);
|
33
|
+
|
34
|
+
for (const block of this.blocks) {
|
35
|
+
await this.processBlockCode(block);
|
36
|
+
}
|
37
|
+
|
38
|
+
this.out.end();
|
39
|
+
}
|
40
|
+
|
41
|
+
public getStream() {
|
42
|
+
return this.out;
|
43
|
+
}
|
44
|
+
|
45
|
+
private handleFileOutput(blockUri: KapetaURI, template: StormFileInfo, data: StormEvent) {
|
46
|
+
switch (data.type) {
|
47
|
+
case 'FILE':
|
48
|
+
template.filename = data.payload.filename;
|
49
|
+
template.content = data.payload.content;
|
50
|
+
this.emitFile(blockUri, data.payload.filename, data.payload.content, data.reason);
|
51
|
+
break;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Generates the code for a block and sends it to the AI
|
57
|
+
*/
|
58
|
+
private async processBlockCode(block: BlockDefinitionInfo) {
|
59
|
+
// Generate the code for the block using the standard codegen templates
|
60
|
+
const generatedResult = await this.generateBlock(block.content, block.screens);
|
61
|
+
if (!generatedResult) {
|
62
|
+
console.warn('No generated result for block', block.uri);
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
const allFiles = this.toStormFiles(generatedResult);
|
67
|
+
|
68
|
+
// Send all the non-ai files to the stream
|
69
|
+
this.emitFiles(block.uri, allFiles);
|
70
|
+
|
71
|
+
const implementFiles = [AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN];
|
72
|
+
|
73
|
+
// Gather the context files. These will be all be passed to the AI
|
74
|
+
const contextFiles: StormFileInfo[] = allFiles.filter(
|
75
|
+
(file) => file.type !== AIFileTypes.IGNORE && !implementFiles.includes(file.type)
|
76
|
+
);
|
77
|
+
|
78
|
+
// Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
|
79
|
+
const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
|
80
|
+
await this.processTemplates(
|
81
|
+
block.uri,
|
82
|
+
stormClient.createServiceImplementation.bind(stormClient),
|
83
|
+
serviceFiles,
|
84
|
+
contextFiles
|
85
|
+
);
|
86
|
+
|
87
|
+
//const uiTemplates: StormFileInfo[] = allFiles.filter((file) => file.type === 'ui');
|
88
|
+
//await this.processTemplates(stormClient.createUIImplementation, uiTemplates, contextFiles);
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Emits the text-based files to the stream
|
93
|
+
*/
|
94
|
+
private emitFiles(uri: KapetaURI, files: StormFileInfo[]) {
|
95
|
+
files.forEach((file) => {
|
96
|
+
if (!file.content || typeof file.content !== 'string') {
|
97
|
+
return;
|
98
|
+
}
|
99
|
+
|
100
|
+
if (file.type === AIFileTypes.SERVICE) {
|
101
|
+
// Don't send the service files to the stream yet
|
102
|
+
// They will need to be implemented by the AI
|
103
|
+
return;
|
104
|
+
}
|
105
|
+
|
106
|
+
if (file.type === AIFileTypes.WEB_SCREEN) {
|
107
|
+
// Don't send the web screen files to the stream yet
|
108
|
+
// They will need to be implemented by the AI
|
109
|
+
return;
|
110
|
+
}
|
111
|
+
|
112
|
+
this.emitFile(uri, file.filename, file.content);
|
113
|
+
});
|
114
|
+
}
|
115
|
+
|
116
|
+
private emitFile(uri: KapetaURI, filename: string, content: string, reason: string = 'File generated') {
|
117
|
+
this.out.emit('data', {
|
118
|
+
type: 'FILE',
|
119
|
+
reason,
|
120
|
+
created: Date.now(),
|
121
|
+
payload: {
|
122
|
+
filename: filename,
|
123
|
+
content: content,
|
124
|
+
blockRef: uri.toNormalizedString(),
|
125
|
+
},
|
126
|
+
} satisfies StormEventFile);
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Sends the template to the AI and processes the response
|
131
|
+
*/
|
132
|
+
private processTemplates(
|
133
|
+
blockUri: KapetaURI,
|
134
|
+
generator: ImplementationGenerator,
|
135
|
+
templates: StormFileInfo[],
|
136
|
+
contextFiles: StormFileInfo[]
|
137
|
+
) {
|
138
|
+
const promises = templates.map(async (templateFile) => {
|
139
|
+
const stream = await generator({
|
140
|
+
context: contextFiles,
|
141
|
+
template: templateFile,
|
142
|
+
prompt: this.userPrompt,
|
143
|
+
});
|
144
|
+
|
145
|
+
stream.on('data', (evt) => this.handleFileOutput(blockUri, templateFile, evt));
|
146
|
+
|
147
|
+
return stream.waitForDone();
|
148
|
+
});
|
149
|
+
|
150
|
+
return Promise.all(promises);
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Converts the generated files to a format that can be sent to the AI
|
155
|
+
*/
|
156
|
+
private toStormFiles(generatedResult: GeneratedResult) {
|
157
|
+
const allFiles: StormFileInfo[] = generatedResult.files.map((file: GeneratedFile) => {
|
158
|
+
if (!file.content) {
|
159
|
+
return {
|
160
|
+
...file,
|
161
|
+
type: AIFileTypes.IGNORE,
|
162
|
+
};
|
163
|
+
}
|
164
|
+
if (typeof file.content !== 'string') {
|
165
|
+
return {
|
166
|
+
...file,
|
167
|
+
type: AIFileTypes.IGNORE,
|
168
|
+
};
|
169
|
+
}
|
170
|
+
|
171
|
+
const rx = /\/\/AI-TYPE:([a-z0-9- _]+)\n/gi;
|
172
|
+
|
173
|
+
const match = rx.exec(file.content);
|
174
|
+
if (!match) {
|
175
|
+
return {
|
176
|
+
...file,
|
177
|
+
type: AIFileTypes.IGNORE,
|
178
|
+
};
|
179
|
+
}
|
180
|
+
const type = match[1].trim() as AIFileTypes;
|
181
|
+
file.content = file.content.replace(rx, '');
|
182
|
+
|
183
|
+
return {
|
184
|
+
...file,
|
185
|
+
type,
|
186
|
+
};
|
187
|
+
});
|
188
|
+
return allFiles;
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Generates the code using codegen for a given block.
|
193
|
+
*/
|
194
|
+
private async generateBlock(yamlContent: Definition, screens: ScreenTemplate[] | undefined) {
|
195
|
+
if (!yamlContent.spec.target?.kind) {
|
196
|
+
//Not all block types have targets
|
197
|
+
return;
|
198
|
+
}
|
199
|
+
|
200
|
+
if (!(await codeGeneratorManager.ensureTarget(yamlContent.spec.target?.kind))) {
|
201
|
+
return;
|
202
|
+
}
|
203
|
+
|
204
|
+
const codeGenerator = new BlockCodeGenerator(yamlContent as BlockDefinition);
|
205
|
+
codeGenerator.withOption('AIContext', STORM_ID);
|
206
|
+
codeGenerator.withOption('AIScreens', screens ?? []);
|
207
|
+
|
208
|
+
return codeGenerator.generate();
|
209
|
+
}
|
210
|
+
}
|