@kapeta/local-cluster-service 0.43.3 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/index.js +2 -0
  3. package/dist/cjs/src/codeGeneratorManager.d.ts +1 -0
  4. package/dist/cjs/src/codeGeneratorManager.js +12 -6
  5. package/dist/cjs/src/middleware/cors.d.ts +1 -0
  6. package/dist/cjs/src/middleware/kapeta.d.ts +1 -0
  7. package/dist/cjs/src/middleware/stringBody.d.ts +1 -0
  8. package/dist/cjs/src/proxy/routes.js +19 -19
  9. package/dist/cjs/src/proxy/types/rest.d.ts +2 -4
  10. package/dist/cjs/src/proxy/types/rest.js +45 -6
  11. package/dist/cjs/src/proxy/types/web.d.ts +2 -4
  12. package/dist/cjs/src/proxy/types/web.js +2 -2
  13. package/dist/cjs/src/storm/codegen.d.ts +36 -0
  14. package/dist/cjs/src/storm/codegen.js +160 -0
  15. package/dist/cjs/src/storm/event-parser.d.ts +70 -0
  16. package/dist/cjs/src/storm/event-parser.js +543 -0
  17. package/dist/cjs/src/storm/events.d.ts +127 -0
  18. package/dist/cjs/src/storm/events.js +6 -0
  19. package/dist/cjs/src/storm/routes.d.ts +7 -0
  20. package/dist/cjs/src/storm/routes.js +109 -0
  21. package/dist/cjs/src/storm/stormClient.d.ts +13 -0
  22. package/dist/cjs/src/storm/stormClient.js +87 -0
  23. package/dist/cjs/src/storm/stream.d.ts +38 -0
  24. package/dist/cjs/src/storm/stream.js +57 -0
  25. package/dist/cjs/src/types.d.ts +5 -2
  26. package/dist/esm/index.js +2 -0
  27. package/dist/esm/src/codeGeneratorManager.d.ts +1 -0
  28. package/dist/esm/src/codeGeneratorManager.js +12 -6
  29. package/dist/esm/src/middleware/cors.d.ts +1 -0
  30. package/dist/esm/src/middleware/kapeta.d.ts +1 -0
  31. package/dist/esm/src/middleware/stringBody.d.ts +1 -0
  32. package/dist/esm/src/proxy/routes.js +19 -19
  33. package/dist/esm/src/proxy/types/rest.d.ts +2 -4
  34. package/dist/esm/src/proxy/types/rest.js +45 -6
  35. package/dist/esm/src/proxy/types/web.d.ts +2 -4
  36. package/dist/esm/src/proxy/types/web.js +2 -2
  37. package/dist/esm/src/storm/codegen.d.ts +36 -0
  38. package/dist/esm/src/storm/codegen.js +160 -0
  39. package/dist/esm/src/storm/event-parser.d.ts +70 -0
  40. package/dist/esm/src/storm/event-parser.js +543 -0
  41. package/dist/esm/src/storm/events.d.ts +127 -0
  42. package/dist/esm/src/storm/events.js +6 -0
  43. package/dist/esm/src/storm/routes.d.ts +7 -0
  44. package/dist/esm/src/storm/routes.js +109 -0
  45. package/dist/esm/src/storm/stormClient.d.ts +13 -0
  46. package/dist/esm/src/storm/stormClient.js +87 -0
  47. package/dist/esm/src/storm/stream.d.ts +38 -0
  48. package/dist/esm/src/storm/stream.js +57 -0
  49. package/dist/esm/src/types.d.ts +5 -2
  50. package/index.ts +2 -0
  51. package/package.json +6 -3
  52. package/src/codeGeneratorManager.ts +17 -8
  53. package/src/proxy/routes.ts +26 -20
  54. package/src/proxy/types/rest.ts +73 -11
  55. package/src/proxy/types/web.ts +3 -5
  56. package/src/storm/codegen.ts +210 -0
  57. package/src/storm/event-parser.ts +688 -0
  58. package/src/storm/events.ts +169 -0
  59. package/src/storm/routes.ts +143 -0
  60. package/src/storm/stormClient.ts +114 -0
  61. package/src/storm/stream.ts +88 -0
  62. package/src/types.ts +5 -2
  63. package/src/utils/BlockInstanceRunner.ts +4 -2
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ export type StormResourceType =
7
+ | 'API'
8
+ | 'DATABASE'
9
+ | 'CLIENT'
10
+ | 'JWTPROVIDER'
11
+ | 'JWTCONSUMER'
12
+ | 'WEBFRAGMENT'
13
+ | 'WEBPAGE'
14
+ | 'SMTPCLIENT'
15
+ | 'EXTERNAL_API'
16
+ | 'SUBSCRIBER'
17
+ | 'PUBLISHER'
18
+ | 'QUEUE'
19
+ | 'EXCHANGE';
20
+
21
+ export type StormBlockType = 'BACKEND' | 'FRONTEND' | 'GATEWAY' | 'MQ' | 'CLI' | 'DESKTOP';
22
+
23
+ export interface StormBlockInfo {
24
+ type: StormBlockType;
25
+ name: string;
26
+ description: string;
27
+ resources: {
28
+ type: StormResourceType;
29
+ name: string;
30
+ description: string;
31
+ }[];
32
+ }
33
+
34
+ export interface StormBlockInfoFilled extends StormBlockInfo {
35
+ apis: string[];
36
+ types: string[];
37
+ models: string[];
38
+ screens: ScreenTemplate[];
39
+ }
40
+
41
+ export interface StormEventCreateBlock {
42
+ type: 'CREATE_BLOCK';
43
+ reason: string;
44
+ created: number;
45
+ payload: StormBlockInfo;
46
+ }
47
+
48
+ export interface StormConnection {
49
+ fromComponent: string;
50
+ fromResource: string;
51
+ fromResourceType: StormResourceType;
52
+ toComponent: string;
53
+ toResource: string;
54
+ toResourceType: StormResourceType;
55
+ }
56
+
57
+ export interface StormEventCreateConnection {
58
+ type: 'CREATE_CONNECTION';
59
+ reason: string;
60
+ created: number;
61
+ payload: StormConnection;
62
+ }
63
+
64
+ export interface StormEventCreatePlanProperties {
65
+ type: 'CREATE_PLAN_PROPERTIES';
66
+ reason: string;
67
+ created: number;
68
+ payload: {
69
+ name: string;
70
+ description: string;
71
+ };
72
+ }
73
+
74
+ export interface StormEventInvalidResponse {
75
+ type: 'INVALID_RESPONSE';
76
+ reason: string;
77
+ created: number;
78
+ payload: {
79
+ error: string;
80
+ };
81
+ }
82
+
83
+ export interface StormEventPlanRetry {
84
+ type: 'PLAN_RETRY' | 'PLAN_RETRY_FAILED';
85
+ reason: string;
86
+ created: number;
87
+ payload: {
88
+ error: string;
89
+ };
90
+ }
91
+
92
+ export interface StormEventCreateDSL {
93
+ type: 'CREATE_API' | 'CREATE_TYPE' | 'CREATE_MODEL';
94
+ reason: string;
95
+ created: number;
96
+ payload: {
97
+ blockName: string;
98
+ content: string;
99
+ };
100
+ }
101
+
102
+ export interface StormEventError {
103
+ type: 'INVALID_RESPONSE' | 'ERROR_INTERNAL';
104
+ reason: string;
105
+ created: number;
106
+ payload: {
107
+ error: string;
108
+ };
109
+ }
110
+
111
+ export interface ScreenTemplate {
112
+ name: string;
113
+ template: string;
114
+ description: string;
115
+ url: string;
116
+ }
117
+
118
+ export interface StormEventScreen {
119
+ type: 'SCREEN';
120
+ reason: string;
121
+ created: number;
122
+ payload: {
123
+ blockName: string;
124
+ name: string;
125
+ template: string;
126
+ description: string;
127
+ url: string;
128
+ };
129
+ }
130
+
131
+ export interface StormEventScreenCandidate {
132
+ type: 'SCREEN_CANDIDATE';
133
+ reason: string;
134
+ created: number;
135
+ payload: {
136
+ name: string;
137
+ description: string;
138
+ url: string;
139
+ };
140
+ }
141
+
142
+ export interface StormEventFile {
143
+ type: 'FILE';
144
+ reason: string;
145
+ created: number;
146
+ payload: {
147
+ filename: string;
148
+ content: string;
149
+ blockRef: string;
150
+ };
151
+ }
152
+
153
+ export interface StormEventDone {
154
+ type: 'DONE';
155
+ created: number;
156
+ }
157
+
158
+ export type StormEvent =
159
+ | StormEventCreateBlock
160
+ | StormEventCreateConnection
161
+ | StormEventCreatePlanProperties
162
+ | StormEventInvalidResponse
163
+ | StormEventPlanRetry
164
+ | StormEventCreateDSL
165
+ | StormEventError
166
+ | StormEventScreen
167
+ | StormEventScreenCandidate
168
+ | StormEventFile
169
+ | StormEventDone;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ import Router from 'express-promise-router';
7
+ import { Response } from 'express';
8
+ import { corsHandler } from '../middleware/cors';
9
+ import { stringBody } from '../middleware/stringBody';
10
+ import { KapetaBodyRequest } from '../types';
11
+ import { StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
12
+ import { stormClient } from './stormClient';
13
+ import { StormEvent } from './events';
14
+ import { resolveOptions, StormEventParser } from './event-parser';
15
+ import { StormCodegen } from './codegen';
16
+
17
+ const router = Router();
18
+
19
+ router.use('/', corsHandler);
20
+ router.use('/', stringBody);
21
+
22
+ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
23
+ const handle = req.params.handle;
24
+
25
+ try {
26
+ const stormOptions = await resolveOptions();
27
+
28
+ const eventParser = new StormEventParser(stormOptions);
29
+
30
+ console.log('Got prompt', req.stringBody);
31
+ const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
32
+ const metaStream = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
33
+
34
+ res.set('Content-Type', 'application/x-ndjson');
35
+
36
+ metaStream.on('data', (data: StormEvent) => {
37
+ eventParser.addEvent(data);
38
+ });
39
+
40
+ await streamStormPartialResponse(metaStream, res);
41
+
42
+ if (!eventParser.isValid()) {
43
+ // We can't continue if the meta stream is invalid
44
+ res.write({
45
+ type: 'ERROR_INTERNAL',
46
+ payload: { error: eventParser.getError() },
47
+ reason: 'Failed to generate system',
48
+ created: Date.now(),
49
+ } satisfies StormEvent);
50
+ res.end();
51
+ return;
52
+ }
53
+ const result = eventParser.toResult(handle);
54
+
55
+ console.log('RESULT\n', JSON.stringify(result, null, 2));
56
+
57
+ const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks);
58
+
59
+ const codegenStream = streamStormPartialResponse(stormCodegen.getStream(), res);
60
+
61
+ await stormCodegen.process();
62
+
63
+ await codegenStream;
64
+
65
+ sendDone(res);
66
+ } catch (err: any) {
67
+ sendError(err, res);
68
+ }
69
+ });
70
+
71
+ router.post('/metadata', async (req: KapetaBodyRequest, res: Response) => {
72
+ const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
73
+ const result = await stormClient.createMetadata(aiRequest.prompt, aiRequest.history);
74
+
75
+ await streamStormResponse(result, res);
76
+ });
77
+
78
+ router.post('/services/implement', async (req: KapetaBodyRequest, res: Response) => {
79
+ const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
80
+ const result = await stormClient.createServiceImplementation(aiRequest.prompt, aiRequest.history);
81
+
82
+ await streamStormResponse(result, res);
83
+ });
84
+
85
+ router.post('/ui/implement', async (req: KapetaBodyRequest, res: Response) => {
86
+ const aiRequest: StormContextRequest<StormFileImplementationPrompt> = JSON.parse(req.stringBody ?? '{}');
87
+ const result = await stormClient.createUIImplementation(aiRequest.prompt, aiRequest.history);
88
+
89
+ await streamStormResponse(result, res);
90
+ });
91
+
92
+ async function streamStormResponse(result: StormStream, res: Response) {
93
+ res.set('Content-Type', 'application/x-ndjson');
94
+
95
+ await streamStormPartialResponse(result, res);
96
+
97
+ sendDone(res);
98
+ }
99
+
100
+ function sendDone(res: Response) {
101
+ res.write(
102
+ JSON.stringify({
103
+ type: 'DONE',
104
+ created: Date.now(),
105
+ } satisfies StormEvent) + '\n'
106
+ );
107
+
108
+ res.end();
109
+ }
110
+
111
+ function sendError(err: Error, res: Response) {
112
+ console.error('Failed to send prompt', err);
113
+ if (res.headersSent) {
114
+ res.write(
115
+ JSON.stringify({
116
+ type: 'ERROR_INTERNAL',
117
+ created: Date.now(),
118
+ payload: { error: err.message },
119
+ reason: 'Failed while sending prompt',
120
+ } satisfies StormEvent) + '\n'
121
+ );
122
+ } else {
123
+ res.status(400).send({ error: err.message });
124
+ }
125
+ }
126
+
127
+ function streamStormPartialResponse(result: StormStream, res: Response) {
128
+ return new Promise<void>((resolve, reject) => {
129
+ result.on('data', (data) => {
130
+ res.write(JSON.stringify(data) + '\n');
131
+ });
132
+
133
+ result.on('error', (err) => {
134
+ reject(err);
135
+ });
136
+
137
+ result.on('end', () => {
138
+ resolve();
139
+ });
140
+ });
141
+ }
142
+
143
+ export default router;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { KapetaAPI } from '@kapeta/nodejs-api-client';
6
+ import { getRemoteUrl } from '../utils/utils';
7
+ import readLine from 'node:readline/promises';
8
+ import { Readable } from 'node:stream';
9
+ import {
10
+ ConversationItem,
11
+ StormContextRequest,
12
+ StormFileImplementationPrompt,
13
+ StormFileInfo,
14
+ StormStream,
15
+ } from './stream';
16
+
17
+ export const STORM_ID = 'storm';
18
+
19
+ class StormClient {
20
+ private readonly _baseUrl: string;
21
+
22
+ constructor() {
23
+ this._baseUrl = getRemoteUrl('ai-service', 'https://ai.kapeta.com');
24
+ }
25
+
26
+ private createOptions(path: string, method: string, body: StormContextRequest): RequestInit & { url: string } {
27
+ const url = `${this._baseUrl}${path}`;
28
+ const headers: { [k: string]: string } = {
29
+ 'Content-Type': 'application/json',
30
+ };
31
+ const api = new KapetaAPI();
32
+ if (api.hasToken()) {
33
+ //headers['Authorization'] = `Bearer ${api.getAccessToken()}`; //TODO: Enable authentication
34
+ }
35
+
36
+ return {
37
+ url,
38
+ method: method,
39
+ body: JSON.stringify(body),
40
+ headers,
41
+ };
42
+ }
43
+
44
+ private async send<T = string>(
45
+ path: string,
46
+ body: StormContextRequest<T>,
47
+ method: string = 'POST'
48
+ ): Promise<StormStream> {
49
+ const stringPrompt = typeof body.prompt === 'string' ? body.prompt : JSON.stringify(body.prompt);
50
+
51
+ const options = this.createOptions(path, method, {
52
+ prompt: stringPrompt,
53
+ history: body.history,
54
+ });
55
+
56
+ const out = new StormStream(stringPrompt, body.history);
57
+
58
+ const response = await fetch(options.url, options);
59
+
60
+ if (response.status !== 200) {
61
+ throw new Error(
62
+ `Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`
63
+ );
64
+ }
65
+
66
+ const jsonLStream = readLine.createInterface(Readable.fromWeb(response.body!));
67
+
68
+ jsonLStream.on('line', (line) => {
69
+ out.addJSONLine(line);
70
+ });
71
+
72
+ jsonLStream.on('error', (error) => {
73
+ out.emit('error', error);
74
+ });
75
+
76
+ jsonLStream.on('pause', () => {
77
+ console.log('paused');
78
+ });
79
+
80
+ jsonLStream.on('resume', () => {
81
+ console.log('resumed');
82
+ });
83
+
84
+ jsonLStream.on('close', () => {
85
+ out.end();
86
+ });
87
+
88
+ return out;
89
+ }
90
+
91
+ public createMetadata(prompt: string, history?: ConversationItem[]) {
92
+ return this.send('/v2/all', {
93
+ history: history ?? [],
94
+ prompt: prompt,
95
+ });
96
+ }
97
+
98
+ public createUIImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]) {
99
+ return this.send<StormFileImplementationPrompt>('/v2/ui/merge', {
100
+ history: history ?? [],
101
+ prompt,
102
+ });
103
+ }
104
+
105
+ public createServiceImplementation(prompt: StormFileImplementationPrompt, history?: ConversationItem[]) {
106
+ console.log('SENDING SERVICE PROMPT', JSON.stringify(prompt, null, 2));
107
+ return this.send<StormFileImplementationPrompt>('/v2/services/merge', {
108
+ history: history ?? [],
109
+ prompt,
110
+ });
111
+ }
112
+ }
113
+
114
+ export const stormClient = new StormClient();
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { EventEmitter } from 'node:events';
6
+ import { StormEvent } from './events';
7
+ import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
8
+
9
+ export class StormStream extends EventEmitter {
10
+ private history: ConversationItem[];
11
+ private lines: string[] = [];
12
+
13
+ constructor(prompt: string = '', history?: ConversationItem[]) {
14
+ super();
15
+ this.history = history ?? [];
16
+ this.history.push({
17
+ role: 'user',
18
+ content: prompt,
19
+ });
20
+ }
21
+
22
+ addJSONLine(line: string) {
23
+ try {
24
+ this.lines.push(line);
25
+ const event = JSON.parse(line);
26
+ if (!event.created) {
27
+ event.created = Date.now();
28
+ }
29
+ this.emit('data', event);
30
+ } catch (e: any) {
31
+ this.emit('error', e);
32
+ }
33
+ }
34
+
35
+ end() {
36
+ this.history.push({
37
+ role: 'model',
38
+ content: this.lines.join('\n'),
39
+ });
40
+ this.emit('end');
41
+ }
42
+
43
+ on(event: 'end', listener: () => void): this;
44
+ on(event: 'error', listener: (e: Error) => void): this;
45
+ on(event: 'data', listener: (data: StormEvent) => void): this;
46
+ on(event: string, listener: (...args: any[]) => void): this {
47
+ return super.on(event, listener);
48
+ }
49
+
50
+ emit(event: 'end'): boolean;
51
+ emit(event: 'error', e: Error): boolean;
52
+ emit(event: 'data', data: StormEvent): boolean;
53
+ emit(eventName: string | symbol, ...args: any[]): boolean {
54
+ return super.emit(eventName, ...args);
55
+ }
56
+
57
+ waitForDone() {
58
+ return new Promise<void>((resolve, reject) => {
59
+ this.on('error', (err) => {
60
+ reject(err);
61
+ });
62
+
63
+ this.on('end', () => {
64
+ resolve();
65
+ });
66
+ });
67
+ }
68
+ }
69
+
70
+ export interface ConversationItem {
71
+ role: 'user' | 'model';
72
+ content: string;
73
+ }
74
+
75
+ export interface StormContextRequest<T = string> {
76
+ history?: ConversationItem[];
77
+ prompt: T;
78
+ }
79
+
80
+ export interface StormFileInfo extends GeneratedFile {
81
+ type: AIFileTypes;
82
+ }
83
+
84
+ export interface StormFileImplementationPrompt {
85
+ context: StormFileInfo[];
86
+ template: StormFileInfo;
87
+ prompt: string;
88
+ }
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ import express from 'express';
7
7
  import { Connection, Resource } from '@kapeta/schemas';
8
8
  import { StringBodyRequest } from './middleware/stringBody';
9
9
  import { KapetaRequest } from './middleware/kapeta';
10
+ import { EnrichedAsset } from './assetManager';
10
11
 
11
12
  export const KIND_RESOURCE_OPERATOR = 'core/resource-type-operator';
12
13
  export const KIND_BLOCK_TYPE = 'core/block-type';
@@ -90,9 +91,11 @@ export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response
90
91
  export interface ProxyRequestInfo {
91
92
  address: string;
92
93
  connection: Connection;
93
- providerResource: Resource;
94
- consumerResource: Resource;
95
94
  consumerPath: string;
95
+ consumerResource: Resource;
96
+ consumerBlockAsset: EnrichedAsset;
97
+ providerResource: Resource;
98
+ providerBlockAsset: EnrichedAsset;
96
99
  }
97
100
 
98
101
  export interface SimpleResponse {
@@ -367,12 +367,14 @@ export class BlockInstanceRunner {
367
367
  // If the bind is optional, we skip it if the host path does not exist
368
368
  options = options.filter((opt) => opt !== 'optional');
369
369
  if (!FSExtra.existsSync(host)) {
370
- console.log(`Skipping optional bind: ${bind} for block: ${blockInstance.id}`)
370
+ console.log(`Skipping optional bind: ${bind} for block: ${blockInstance.id}`);
371
371
  return '';
372
372
  }
373
373
  }
374
374
 
375
- return `${toLocalBindVolume(host)}:${container}${options.length > 0 ? ':' + options.join(':') : ''}`;
375
+ return `${toLocalBindVolume(host)}:${container}${
376
+ options.length > 0 ? ':' + options.join(':') : ''
377
+ }`;
376
378
  }).filter((bind: string) => Boolean(bind)),
377
379
  ],
378
380
  PortBindings,