@kapeta/local-cluster-service 0.61.2 → 0.62.1

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.
@@ -12,8 +12,17 @@ import { corsHandler } from '../middleware/cors';
12
12
  import { stringBody } from '../middleware/stringBody';
13
13
  import { KapetaBodyRequest } from '../types';
14
14
  import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
15
- import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt, UIPageEditRequest } from './stormClient';
15
+
16
+ import {
17
+ ConversationIdHeader,
18
+ stormClient,
19
+ UIPagePrompt,
20
+ UIPageEditPrompt,
21
+ UIPageEditRequest,
22
+ UIPageSamplePrompt,
23
+ } from './stormClient';
16
24
  import { Page, StormEvent, StormEventPage, StormEventPhaseType, UIShell, UserJourneyScreen } from './events';
25
+
17
26
  import {
18
27
  createPhaseEndEvent,
19
28
  createPhaseStartEvent,
@@ -25,8 +34,17 @@ import { StormCodegen } from './codegen';
25
34
  import { assetManager } from '../assetManager';
26
35
  import uuid from 'node-uuid';
27
36
  import { PromiseQueue } from './PromiseQueue';
28
- import { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
37
+ import {
38
+ readConversationFromFile,
39
+ readPageFromDisk,
40
+ readPageFromDiskAsString,
41
+ SystemIdHeader,
42
+ writeConversationToFile,
43
+ writePageToDisk,
44
+ } from './page-utils';
29
45
  import { UIServer } from './UIServer';
46
+ import { PageQueue } from './PageGenerator';
47
+ import FSExtra from 'fs-extra';
30
48
 
31
49
  const UI_SERVERS: { [key: string]: UIServer } = {};
32
50
  const router = Router();
@@ -34,6 +52,8 @@ const router = Router();
34
52
  router.use('/', corsHandler);
35
53
  router.use('/', stringBody);
36
54
 
55
+ const samplesBaseDir = Path.join(__dirname, 'samples');
56
+
37
57
  function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
38
58
  if (screenData.type === 'PAGE') {
39
59
  const server: UIServer | undefined = UI_SERVERS[mainConversationId];
@@ -75,22 +95,25 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
75
95
  const aiRequest: UIPagePrompt = JSON.parse(req.stringBody ?? '{}');
76
96
  aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
77
97
 
78
- const screenStream = await stormClient.createUIPage(aiRequest, conversationId);
98
+ res.set('Content-Type', 'application/x-ndjson');
99
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
100
+ res.set(ConversationIdHeader, conversationId);
101
+
102
+ const parentConversationId = systemId ?? '';
79
103
 
104
+ const queue = new PageQueue(parentConversationId, 5);
80
105
  onRequestAborted(req, res, () => {
81
- screenStream.abort();
106
+ queue.cancel();
82
107
  });
83
108
 
84
- res.set('Content-Type', 'application/x-ndjson');
85
- res.set('Access-Control-Expose-Headers', ConversationIdHeader);
86
- res.set(ConversationIdHeader, screenStream.getConversationId());
109
+ await queue.addPrompt(aiRequest);
87
110
 
88
111
  const promises: Promise<void>[] = [];
89
- screenStream.on('data', (data: StormEvent) => {
112
+ queue.on('page', (data) => {
90
113
  switch (data.type) {
91
114
  case 'PAGE':
92
115
  console.log('Processing page event', data);
93
- data.payload.conversationId = screenStream.getConversationId();
116
+
94
117
  if (systemId) {
95
118
  promises.push(sendPageEvent(systemId, data, res));
96
119
  }
@@ -99,7 +122,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
99
122
  sendEvent(res, data);
100
123
  });
101
124
 
102
- await waitForStormStream(screenStream);
125
+ await queue.wait();
103
126
  await Promise.allSettled(promises);
104
127
 
105
128
  sendDone(res);
@@ -125,6 +148,93 @@ router.delete('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
125
148
  }
126
149
  });
127
150
 
151
+ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Response) => {
152
+ const handle = req.params.handle as string;
153
+ try {
154
+ const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
155
+
156
+ const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
157
+
158
+ const landingPagesStream = await stormClient.createUILandingPages(aiRequest.prompt, conversationId);
159
+
160
+ onRequestAborted(req, res, () => {
161
+ landingPagesStream.abort();
162
+ });
163
+
164
+ res.set('Content-Type', 'application/x-ndjson');
165
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
166
+
167
+ res.set(ConversationIdHeader, landingPagesStream.getConversationId());
168
+
169
+ const promises: { [key: string]: Promise<void> } = {};
170
+ const pageEventPromises: Promise<void>[] = [];
171
+ const systemId = landingPagesStream.getConversationId();
172
+ const pageQueue = new PageQueue(systemId, 5);
173
+
174
+ landingPagesStream.on('data', async (data: StormEvent) => {
175
+ try {
176
+ sendEvent(res, data);
177
+ if (data.type !== 'LANDING_PAGE') {
178
+ return;
179
+ }
180
+
181
+ if (landingPagesStream.isAborted()) {
182
+ return;
183
+ }
184
+ const landingPage = data.payload;
185
+ if (landingPage.name in promises) {
186
+ return;
187
+ }
188
+
189
+ // We add the landing pages to the prompt queue.
190
+ // These will then be analysed - creating further pages as needed
191
+ promises[landingPage.name] = pageQueue.addPrompt({
192
+ prompt: landingPage.create_prompt,
193
+ method: 'GET',
194
+ path: landingPage.path,
195
+ description: landingPage.create_prompt,
196
+ name: landingPage.name,
197
+ title: landingPage.title,
198
+ filename: landingPage.filename,
199
+ storage_prefix: systemId + '_',
200
+ });
201
+ } catch (e) {
202
+ console.error('Failed to process event', e);
203
+ }
204
+ });
205
+
206
+ UI_SERVERS[systemId] = new UIServer(systemId);
207
+ await UI_SERVERS[systemId].start();
208
+
209
+ onRequestAborted(req, res, () => {
210
+ pageQueue.cancel();
211
+ });
212
+
213
+ pageQueue.on('page', (screenData: StormEventPage) => {
214
+ pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
215
+ });
216
+
217
+ pageQueue.on('event', (screenData: StormEvent) => {
218
+ sendEvent(res, screenData);
219
+ });
220
+
221
+ await waitForStormStream(landingPagesStream);
222
+ await pageQueue.wait();
223
+ await Promise.allSettled(pageEventPromises);
224
+
225
+ if (landingPagesStream.isAborted()) {
226
+ return;
227
+ }
228
+
229
+ sendDone(res);
230
+ } catch (err) {
231
+ sendError(err as Error, res);
232
+ if (!res.closed) {
233
+ res.end();
234
+ }
235
+ }
236
+ });
237
+
128
238
  router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
129
239
  const handle = req.params.handle as string;
130
240
  try {
@@ -223,75 +333,40 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
223
333
  await UI_SERVERS[outerConversationId].start();
224
334
 
225
335
  // Get the pages (5 at a time)
226
- const queue = new PromiseQueue(5);
227
-
336
+ const queue = new PageQueue(outerConversationId, 5);
337
+ const pagePromises: Promise<void>[] = [];
228
338
  onRequestAborted(req, res, () => {
229
339
  queue.cancel();
230
340
  });
231
341
 
342
+ const pageEventPromises: Promise<void>[] = [];
343
+ queue.on('page', (screenData: StormEventPage) => {
344
+ pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
345
+ });
346
+
347
+ queue.on('event', (screenData: StormEvent) => {
348
+ sendEvent(res, screenData);
349
+ });
350
+
232
351
  for (const screen of Object.values(uniqueUserJourneyScreens)) {
233
- void queue.add(
234
- () =>
235
- new Promise(async (resolve, reject) => {
236
- try {
237
- const innerConversationId = uuid.v4();
238
- const screenStream = await stormClient.createUIPage(
239
- {
240
- prompt: screen.requirements,
241
- method: screen.method,
242
- path: screen.path,
243
- description: screen.requirements,
244
- name: screen.name,
245
- title: screen.title,
246
- filename: screen.filename,
247
- storage_prefix: outerConversationId + '_',
248
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
249
- },
250
- innerConversationId
251
- );
252
-
253
- const promiseList: Promise<void>[] = [];
254
- screenStream.on('data', (screenData: StormEvent) => {
255
- if (screenData.type === 'PAGE') {
256
- promiseList.push(
257
- sendPageEvent(
258
- outerConversationId,
259
- {
260
- ...screenData,
261
- payload: {
262
- ...screenData.payload,
263
- conversationId: innerConversationId,
264
- },
265
- },
266
- res
267
- )
268
- );
269
- } else {
270
- sendEvent(res, screenData);
271
- }
272
- });
273
-
274
- screenStream.on('end', async () => {
275
- try {
276
- await Promise.allSettled(promiseList).finally(() => resolve(true));
277
- } catch (error) {
278
- console.error('Failed to process screen', error);
279
- }
280
- });
281
-
282
- screenStream.on('error', (error) => {
283
- console.error('Error on screenStream', error);
284
- screenStream.abort();
285
- });
286
- } catch (e) {
287
- console.error('Failed to process screen', e);
288
- reject(e);
289
- }
290
- })
352
+ pagePromises.push(
353
+ queue.addPrompt({
354
+ prompt: screen.requirements,
355
+ method: screen.method,
356
+ path: screen.path,
357
+ description: screen.requirements,
358
+ name: screen.name,
359
+ title: screen.title,
360
+ filename: screen.filename,
361
+ storage_prefix: outerConversationId + '_',
362
+ shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
363
+ })
291
364
  );
292
365
  }
293
366
 
294
367
  await queue.wait();
368
+ await Promise.allSettled(pagePromises);
369
+ await Promise.allSettled(pageEventPromises);
295
370
 
296
371
  if (userJourneysStream.isAborted()) {
297
372
  return;
@@ -408,6 +483,7 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
408
483
  const result = await eventParser.processEvent(handle, data);
409
484
 
410
485
  switch (data.type) {
486
+ case 'API_STREAM_START':
411
487
  case 'CREATE_API':
412
488
  case 'CREATE_MODEL':
413
489
  case 'CREATE_TYPE':
@@ -581,6 +657,7 @@ function sendError(err: Error, res: Response) {
581
657
  res.status(400).send({ error: err.message });
582
658
  }
583
659
  }
660
+
584
661
  function waitForStormStream(result: StormStream) {
585
662
  return result.waitForDone();
586
663
  }
@@ -44,6 +44,10 @@ export interface UIPagePrompt {
44
44
  shell_page?: string;
45
45
  }
46
46
 
47
+ export interface UIPageSamplePrompt extends UIPagePrompt {
48
+ variantId: string;
49
+ }
50
+
47
51
  export interface UIPageEditPrompt {
48
52
  planDescription: string;
49
53
  blockDescription: string;
@@ -168,14 +172,28 @@ class StormClient {
168
172
  public createUIShells(prompt: UIShellsPrompt, conversationId?: string) {
169
173
  return this.send('/v2/ui/shells', {
170
174
  prompt: JSON.stringify(prompt),
175
+ });
176
+ }
177
+
178
+ public createUILandingPages(prompt: string, conversationId?: string) {
179
+ return this.send('/v2/ui/landing-pages', {
180
+ prompt: prompt,
171
181
  conversationId,
172
182
  });
173
183
  }
174
184
 
175
- public createUIPage(prompt: UIPagePrompt, conversationId?: string) {
185
+ public createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]) {
176
186
  return this.send('/v2/ui/page', {
177
187
  prompt: prompt,
178
188
  conversationId,
189
+ history,
190
+ });
191
+ }
192
+
193
+ public classifyUIReferences(prompt: string, conversationId?: string) {
194
+ return this.send('/v2/ui/references', {
195
+ prompt: prompt,
196
+ conversationId,
179
197
  });
180
198
  }
181
199
 
@@ -234,6 +252,20 @@ class StormClient {
234
252
  conversationId: conversationId,
235
253
  });
236
254
  }
255
+
256
+ async deleteUIPageConversation(conversationId: string) {
257
+ const options = await this.createOptions('/v2/ui/page', 'DELETE', {
258
+ prompt: '',
259
+ conversationId: conversationId,
260
+ });
261
+
262
+ const response = await fetch(options.url, {
263
+ method: options.method,
264
+ headers: options.headers,
265
+ });
266
+
267
+ return response.text();
268
+ }
237
269
  }
238
270
 
239
271
  export const stormClient = new StormClient();
@@ -37,6 +37,7 @@ export class StormStream extends EventEmitter {
37
37
  this.emit('data', event);
38
38
  } catch (e: any) {
39
39
  this.emit('error', e);
40
+ console.warn('Failed to parse JSON line', e, line);
40
41
  }
41
42
  }
42
43
 
@@ -98,6 +99,7 @@ export interface ConversationItem {
98
99
 
99
100
  export interface StormContextRequest<T = string> {
100
101
  conversationId?: string;
102
+ history?: ConversationItem[];
101
103
  prompt: T;
102
104
  }
103
105