@kapeta/local-cluster-service 0.49.0 → 0.51.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.
@@ -11,12 +11,17 @@ import { stringBody } from '../middleware/stringBody';
11
11
  import { KapetaBodyRequest } from '../types';
12
12
  import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
13
13
  import { ConversationIdHeader, stormClient } from './stormClient';
14
- import { StormEvent } from './events';
15
- import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
14
+ import { StormEvent, StormEventPhaseType } from './events';
15
+ import {
16
+ createPhaseEndEvent,
17
+ createPhaseStartEvent,
18
+ resolveOptions,
19
+ StormDefinitions,
20
+ StormEventParser,
21
+ } from './event-parser';
16
22
  import { StormCodegen } from './codegen';
17
23
  import { assetManager } from '../assetManager';
18
24
  import Path from 'path';
19
- import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
20
25
 
21
26
  const router = Router();
22
27
 
@@ -36,18 +41,48 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
36
41
  const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
37
42
  const metaStream = await stormClient.createMetadata(aiRequest.prompt, conversationId);
38
43
 
44
+ onRequestAborted(req, res, () => {
45
+ metaStream.abort();
46
+ });
47
+
39
48
  res.set('Content-Type', 'application/x-ndjson');
40
49
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
41
50
  res.set(ConversationIdHeader, metaStream.getConversationId());
42
51
 
52
+ let currentPhase = StormEventPhaseType.META;
53
+
43
54
  metaStream.on('data', (data: StormEvent) => {
44
55
  const result = eventParser.processEvent(req.params.handle, data);
45
56
 
57
+ switch (data.type) {
58
+ case 'CREATE_API':
59
+ case 'CREATE_MODEL':
60
+ case 'CREATE_TYPE':
61
+ if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
62
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
63
+ currentPhase = StormEventPhaseType.DEFINITIONS;
64
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
65
+ }
66
+ break;
67
+ }
68
+
46
69
  sendEvent(res, data);
47
70
  sendDefinitions(res, result);
48
71
  });
49
72
 
50
- await waitForStormStream(metaStream);
73
+ try {
74
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.META));
75
+
76
+ await waitForStormStream(metaStream);
77
+ } finally {
78
+ if (!metaStream.isAborted()) {
79
+ sendEvent(res, createPhaseEndEvent(currentPhase));
80
+ }
81
+ }
82
+
83
+ if (metaStream.isAborted()) {
84
+ return;
85
+ }
51
86
 
52
87
  if (!eventParser.isValid()) {
53
88
  // We can't continue if the meta stream is invalid
@@ -63,27 +98,44 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
63
98
 
64
99
  const result = eventParser.toResult(handle);
65
100
 
101
+ if (metaStream.isAborted()) {
102
+ return;
103
+ }
104
+
66
105
  sendDefinitions(res, result);
67
106
 
68
107
  if (!req.query.skipCodegen) {
69
- const stormCodegen = new StormCodegen(
70
- metaStream.getConversationId(),
71
- aiRequest.prompt,
72
- result.blocks,
73
- eventParser.getEvents()
74
- );
75
-
76
- const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
77
-
78
- await stormCodegen.process();
79
-
80
- await codegenPromise;
108
+ try {
109
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENTATION));
110
+ const stormCodegen = new StormCodegen(
111
+ metaStream.getConversationId(),
112
+ aiRequest.prompt,
113
+ result.blocks,
114
+ eventParser.getEvents()
115
+ );
116
+
117
+ onRequestAborted(req, res, () => {
118
+ stormCodegen.abort();
119
+ });
120
+
121
+ const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
122
+
123
+ await stormCodegen.process();
124
+
125
+ await codegenPromise;
126
+ } finally {
127
+ if (!metaStream.isAborted()) {
128
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.IMPLEMENTATION));
129
+ }
130
+ }
81
131
  }
82
132
 
83
133
  sendDone(res);
84
134
  } catch (err: any) {
85
135
  sendError(err, res);
86
- res.end();
136
+ if (!res.closed) {
137
+ res.end();
138
+ }
87
139
  }
88
140
  });
89
141
 
@@ -93,19 +145,13 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
93
145
  try {
94
146
  const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
95
147
 
96
- console.log('Creating block at', ymlPath);
97
-
98
148
  const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
99
149
 
100
150
  if (await FS.pathExists(createRequest.tmpPath)) {
101
- console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
102
-
103
151
  await FS.move(createRequest.tmpPath, createRequest.newPath, {
104
152
  overwrite: true,
105
153
  });
106
154
 
107
- console.log('Updating asset', asset.ref);
108
-
109
155
  res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
110
156
  } else {
111
157
  res.send(asset);
@@ -125,6 +171,9 @@ function sendDefinitions(res: Response, result: StormDefinitions) {
125
171
  }
126
172
 
127
173
  function sendDone(res: Response) {
174
+ if (res.closed) {
175
+ return;
176
+ }
128
177
  sendEvent(res, {
129
178
  type: 'DONE',
130
179
  created: Date.now(),
@@ -134,6 +183,9 @@ function sendDone(res: Response) {
134
183
  }
135
184
 
136
185
  function sendError(err: Error, res: Response) {
186
+ if (res.closed) {
187
+ return;
188
+ }
137
189
  console.error('Failed to send prompt', err);
138
190
  if (res.headersSent) {
139
191
  sendEvent(res, {
@@ -175,7 +227,19 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
175
227
  }
176
228
 
177
229
  function sendEvent(res: Response, evt: StormEvent) {
230
+ if (res.closed) {
231
+ return;
232
+ }
178
233
  res.write(JSON.stringify(evt) + '\n');
179
234
  }
180
235
 
236
+ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () => void) {
237
+ req.on('close', () => {
238
+ onAborted();
239
+ });
240
+ res.on('close', () => {
241
+ onAborted();
242
+ });
243
+ }
244
+
181
245
  export default router;
@@ -13,6 +13,7 @@ import {
13
13
  StormStream,
14
14
  StormUIImplementationPrompt,
15
15
  } from './stream';
16
+ import { getRawAsset } from 'node:sea';
16
17
 
17
18
  export const STORM_ID = 'storm';
18
19
 
@@ -60,6 +61,9 @@ class StormClient {
60
61
  conversationId: body.conversationId,
61
62
  });
62
63
 
64
+ const abort = new AbortController();
65
+ options.signal = abort.signal;
66
+
63
67
  const response = await fetch(options.url, options);
64
68
 
65
69
  if (response.status !== 200) {
@@ -69,7 +73,6 @@ class StormClient {
69
73
  }
70
74
 
71
75
  const conversationId = response.headers.get(ConversationIdHeader);
72
- console.log('Received conversationId', conversationId);
73
76
 
74
77
  const out = new StormStream(stringPrompt, conversationId);
75
78
 
@@ -87,6 +90,10 @@ class StormClient {
87
90
  out.end();
88
91
  });
89
92
 
93
+ out.on('aborted', () => {
94
+ abort.abort();
95
+ });
96
+
90
97
  return out;
91
98
  }
92
99
 
@@ -10,6 +10,7 @@ import { BlockDefinition } from '@kapeta/schemas';
10
10
  export class StormStream extends EventEmitter {
11
11
  private conversationId: string = '';
12
12
  private lines: string[] = [];
13
+ private aborted: boolean = false;
13
14
 
14
15
  constructor(prompt: string = '', conversationId?: string | null) {
15
16
  super();
@@ -20,6 +21,10 @@ export class StormStream extends EventEmitter {
20
21
  return this.conversationId;
21
22
  }
22
23
 
24
+ isAborted() {
25
+ return this.aborted;
26
+ }
27
+
23
28
  addJSONLine(line: string) {
24
29
  try {
25
30
  this.lines.push(line);
@@ -38,6 +43,7 @@ export class StormStream extends EventEmitter {
38
43
  }
39
44
 
40
45
  on(event: 'end', listener: () => void): this;
46
+ on(event: 'aborted', listener: () => void): this;
41
47
  on(event: 'error', listener: (e: Error) => void): this;
42
48
  on(event: 'data', listener: (data: StormEvent) => void): this;
43
49
  on(event: string, listener: (...args: any[]) => void): this {
@@ -45,6 +51,7 @@ export class StormStream extends EventEmitter {
45
51
  }
46
52
 
47
53
  emit(event: 'end'): boolean;
54
+ emit(event: 'aborted'): void;
48
55
  emit(event: 'error', e: Error): boolean;
49
56
  emit(event: 'data', data: StormEvent): boolean;
50
57
  emit(eventName: string | symbol, ...args: any[]): boolean {
@@ -62,6 +69,14 @@ export class StormStream extends EventEmitter {
62
69
  });
63
70
  });
64
71
  }
72
+
73
+ abort() {
74
+ if (this.aborted) {
75
+ return;
76
+ }
77
+ this.aborted = true;
78
+ this.emit('aborted');
79
+ }
65
80
  }
66
81
 
67
82
  export interface ConversationItem {