@kapeta/local-cluster-service 0.67.2 → 0.67.4

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.
@@ -20,16 +20,22 @@ export interface ImagePrompt {
20
20
  content: string;
21
21
  }
22
22
 
23
+ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & { shellType?: 'public' | 'admin' | 'user' };
24
+
23
25
  export class PageQueue extends EventEmitter {
24
26
  private readonly queue: PromiseQueue;
25
27
  private readonly systemId: string;
28
+ private readonly systemPrompt: string;
26
29
  private readonly references: Map<string, PageGenerator> = new Map();
30
+ private readonly pages: Map<string, string> = new Map();
31
+ private readonly images: Map<string, string> = new Map();
27
32
  private uiShells: UIShell[] = [];
28
33
  private theme = '';
29
34
 
30
- constructor(systemId: string, concurrency: number = 5) {
35
+ constructor(systemId: string, systemPrompt: string, concurrency: number = 5) {
31
36
  super();
32
37
  this.systemId = systemId;
38
+ this.systemPrompt = systemPrompt;
33
39
  this.queue = new PromiseQueue(concurrency);
34
40
  }
35
41
 
@@ -56,82 +62,145 @@ export class PageQueue extends EventEmitter {
56
62
  this.theme = theme;
57
63
  }
58
64
 
59
- public addPrompt(
60
- initialPrompt: Omit<UIPagePrompt, 'shell_page'>,
61
- conversationId: string = uuid.v4(),
62
- overwrite: boolean = false
63
- ) {
64
- if (!overwrite && this.references.has(initialPrompt.path)) {
65
+ private hasPrompt(path: string) {
66
+ if (this.references.has(path)) {
65
67
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
66
- return Promise.resolve();
68
+ return true;
67
69
  }
68
70
 
69
- if (!overwrite && hasPageOnDisk(this.systemId, initialPrompt.method, initialPrompt.path)) {
71
+ if (hasPageOnDisk(this.systemId, 'GET', path)) {
70
72
  //console.log('Ignoring prompt with existing page', initialPrompt.path);
73
+ return true;
74
+ }
75
+
76
+ return false;
77
+ }
78
+
79
+ public addPrompt(initialPrompt: InitialPrompt, conversationId: string = uuid.v4(), overwrite: boolean = false) {
80
+ if (!overwrite && this.hasPrompt(initialPrompt.path)) {
81
+ //console.log('Ignoring duplicate prompt', initialPrompt.path);
71
82
  return Promise.resolve();
72
83
  }
73
84
 
74
85
  const prompt: UIPagePrompt = {
75
86
  ...initialPrompt,
76
87
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
88
+ prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
77
89
  theme: this.theme,
78
90
  };
79
91
 
80
92
  const generator = new PageGenerator(prompt, conversationId);
81
93
  this.references.set(prompt.path, generator);
94
+ this.pages.set(prompt.path, prompt.description);
82
95
 
83
96
  return this.addPageGenerator(generator);
84
97
  }
98
+ private getPrefix(): string {
99
+ let promptPrefix = '';
100
+ if (this.systemPrompt) {
101
+ promptPrefix = `For a system with this description: ${this.systemPrompt}\n`;
102
+ }
103
+ return promptPrefix;
104
+ }
85
105
 
86
- private async addPageGenerator(generator: PageGenerator) {
87
- generator.on('event', (event: StormEvent) => this.emit('event', event));
88
- generator.on('page_refs', async ({ event, references }) => {
89
- const promises = references.map(async (reference) => {
90
- if (
91
- reference.url.startsWith('#') ||
92
- reference.url.startsWith('javascript:') ||
93
- reference.url.startsWith('http://') ||
94
- reference.url.startsWith('https://')
95
- ) {
106
+ private wrapPagePrompt(pagePath: string, prompt: string): string {
107
+ let promptPrefix = this.getPrefix();
108
+ let promptPostfix = '';
109
+
110
+ if (this.pages.size > 0) {
111
+ promptPostfix = `\nThe following pages are already implemented:\n`;
112
+ this.pages.forEach((description, path) => {
113
+ if (pagePath === path) {
96
114
  return;
97
115
  }
116
+ promptPostfix += `- PAGE: '${path}' -> ${description}.\n`;
117
+ });
118
+ }
98
119
 
99
- switch (reference.type) {
100
- case 'image':
101
- await this.addImagePrompt({
102
- ...reference,
103
- content: event.payload.content,
104
- });
105
- break;
106
- case 'css':
107
- case 'javascript':
108
- //console.log('Ignoring reference', reference);
109
- break;
110
- case 'html':
111
- //console.log('Adding page generator for', reference);
112
- const paths = Array.from(this.references.keys());
113
- this.addPrompt({
114
- name: reference.name,
115
- title: reference.title,
116
- path: reference.url,
117
- method: 'GET',
118
- storage_prefix: this.systemId + '_',
119
- prompt:
120
- `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
121
- `The page was referenced from this page: \`\`\`html\n${event.payload.content}\n\`\`\`\n` +
122
- (paths.length > 0
123
- ? `\nThese paths are already implemented:\n- ${paths.join('\n - ')}\n\n`
124
- : ''),
125
- description: reference.description,
126
- filename: '',
127
- theme: this.theme,
128
- });
129
- break;
130
- }
120
+ if (this.images.size > 0) {
121
+ promptPostfix += `\nThe following images already exist:\n`;
122
+ this.images.forEach((description, path) => {
123
+ promptPostfix += `- IMAGE: '${path}' -> ${description}.\n`;
131
124
  });
125
+ }
126
+
127
+ return promptPrefix + prompt + promptPostfix;
128
+ }
132
129
 
133
- await Promise.allSettled(promises);
134
- this.emit('page', event);
130
+ private async addPageGenerator(generator: PageGenerator) {
131
+ generator.on('event', (event: StormEvent) => this.emit('event', event));
132
+ generator.on('page_refs', async ({ event, references, future }) => {
133
+ try {
134
+ const initialPrompts: InitialPrompt[] = [];
135
+ let promises = references.map(async (reference) => {
136
+ if (
137
+ reference.url.startsWith('#') ||
138
+ reference.url.startsWith('javascript:') ||
139
+ reference.url.startsWith('http://') ||
140
+ reference.url.startsWith('https://')
141
+ ) {
142
+ return;
143
+ }
144
+
145
+ switch (reference.type) {
146
+ case 'image':
147
+ await this.addImagePrompt({
148
+ ...reference,
149
+ content: event.payload.content,
150
+ });
151
+ break;
152
+ case 'css':
153
+ case 'javascript':
154
+ //console.log('Ignoring reference', reference);
155
+ break;
156
+ case 'html':
157
+ //console.log('Adding page generator for', reference);
158
+ this.pages.set(reference.url, reference.description);
159
+
160
+ initialPrompts.push({
161
+ name: reference.name,
162
+ title: reference.title,
163
+ path: reference.url,
164
+ method: 'GET',
165
+ storage_prefix: this.systemId + '_',
166
+ prompt:
167
+ `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
168
+ `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
169
+ description: reference.description,
170
+ filename: '',
171
+ theme: this.theme,
172
+ });
173
+ break;
174
+ }
175
+ });
176
+
177
+ await Promise.allSettled(promises);
178
+ initialPrompts.forEach((prompt) => {
179
+ if (!this.hasPrompt(prompt.path)) {
180
+ this.emit('page', {
181
+ type: 'PAGE',
182
+ reason: 'reference',
183
+ created: Date.now(),
184
+ payload: {
185
+ name: prompt.name,
186
+ title: prompt.title,
187
+ filename: '',
188
+ method: 'GET',
189
+ path: prompt.path,
190
+ prompt: prompt.description,
191
+ conversationId: '',
192
+ content: '',
193
+ description: prompt.description,
194
+ },
195
+ });
196
+ }
197
+ this.addPrompt(prompt);
198
+ });
199
+
200
+ this.emit('page', event);
201
+ } finally {
202
+ future.resolve();
203
+ }
135
204
  });
136
205
  return this.queue.add(() => generator.generate());
137
206
  }
@@ -145,10 +214,18 @@ export class PageQueue extends EventEmitter {
145
214
  }
146
215
 
147
216
  private async addImagePrompt(prompt: ImagePrompt) {
217
+ if (this.images.has(prompt.url)) {
218
+ //console.log('Ignoring duplicate image prompt', prompt);
219
+ return;
220
+ }
221
+ this.images.set(prompt.url, prompt.description);
222
+ const prefix = this.getPrefix();
148
223
  const result = await stormClient.createImage(
149
- `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim()
224
+ prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim()
150
225
  );
151
226
 
227
+ //console.log('Adding image prompt', prompt);
228
+
152
229
  const futures: FuturePromise<void>[] = [];
153
230
 
154
231
  result.on('data', async (event: StormEvent) => {
@@ -172,7 +249,7 @@ export class PageQueue extends EventEmitter {
172
249
 
173
250
  export class PageGenerator extends EventEmitter {
174
251
  private readonly conversationId: string;
175
- private prompt: UIPagePrompt;
252
+ public readonly prompt: UIPagePrompt;
176
253
 
177
254
  constructor(prompt: UIPagePrompt, conversationId: string = uuid.v4()) {
178
255
  super();
@@ -183,7 +260,11 @@ export class PageGenerator extends EventEmitter {
183
260
  on(event: 'event', listener: (data: StormEvent) => void): this;
184
261
  on(
185
262
  event: 'page_refs',
186
- listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => void
263
+ listener: (data: {
264
+ event: StormEventPage;
265
+ references: ReferenceClassification[];
266
+ future: FuturePromise<void>;
267
+ }) => void
187
268
  ): this;
188
269
 
189
270
  on(event: string, listener: (...args: any[]) => void): this {
@@ -191,16 +272,18 @@ export class PageGenerator extends EventEmitter {
191
272
  }
192
273
 
193
274
  emit(type: 'event', event: StormEvent): boolean;
194
- emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
275
+ emit(
276
+ type: 'page_refs',
277
+ event: { event: StormEventPage; references: ReferenceClassification[]; future: FuturePromise<void> }
278
+ ): boolean;
195
279
  emit(eventName: string | symbol, ...args: any[]): boolean {
196
280
  return super.emit(eventName, ...args);
197
281
  }
198
282
 
199
283
  public async generate() {
200
284
  return new Promise<void>(async (resolve) => {
201
- const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
202
-
203
285
  const promises: Promise<void>[] = [];
286
+ const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
204
287
 
205
288
  screenStream.on('data', (event: StormEvent) => {
206
289
  if (event.type === 'PAGE') {
@@ -210,10 +293,14 @@ export class PageGenerator extends EventEmitter {
210
293
  (async () => {
211
294
  const references = await this.resolveReferences(event.payload.content);
212
295
  //console.log('Resolved references for page', references, event.payload);
296
+ const future = createFuture();
213
297
  this.emit('page_refs', {
214
298
  event,
215
299
  references,
300
+ future,
216
301
  });
302
+
303
+ await future.promise;
217
304
  })()
218
305
  );
219
306
  return;
@@ -222,11 +309,9 @@ export class PageGenerator extends EventEmitter {
222
309
  this.emit('event', event);
223
310
  });
224
311
 
225
- screenStream.on('end', () => {
226
- Promise.allSettled(promises).finally(resolve);
227
- });
228
-
229
312
  await screenStream.waitForDone();
313
+ await Promise.all(promises);
314
+ resolve();
230
315
  });
231
316
  }
232
317
 
@@ -17,7 +17,6 @@ import {
17
17
  ConversationIdHeader,
18
18
  stormClient,
19
19
  UIPagePrompt,
20
- UIPageEditPrompt,
21
20
  UIPageEditRequest,
22
21
  BasePromptRequest,
23
22
  UIPageVoteRequest,
@@ -35,17 +34,11 @@ import {
35
34
  import { StormCodegen } from './codegen';
36
35
  import { assetManager } from '../assetManager';
37
36
  import uuid from 'node-uuid';
38
- import {
39
- readPageFromDisk,
40
- readPageFromDiskAsString,
41
- SystemIdHeader,
42
- writeAssetToDisk,
43
- writeImageToDisk,
44
- writePageToDisk,
45
- } from './page-utils';
37
+ import { readPageFromDisk, SystemIdHeader, writeAssetToDisk, writeImageToDisk, writePageToDisk } from './page-utils';
46
38
  import { UIServer } from './UIServer';
47
39
  import { randomUUID } from 'crypto';
48
40
  import { ImagePrompt, PageQueue } from './PageGenerator';
41
+ import { createFuture } from './PromiseQueue';
49
42
 
50
43
  const UI_SERVERS: { [key: string]: UIServer } = {};
51
44
  const router = Router();
@@ -74,7 +67,7 @@ function convertPageEvent(screenData: StormEvent, innerConversationId: string, m
74
67
  description: screenData.payload.description,
75
68
  prompt: screenData.payload.prompt,
76
69
  path: screenData.payload.path,
77
- url: server ? server.resolveUrl(screenData) : '',
70
+ url: server && screenData.payload.content ? server.resolveUrl(screenData) : '',
78
71
  method: screenData.payload.method,
79
72
  conversationId: innerConversationId,
80
73
  },
@@ -102,7 +95,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
102
95
 
103
96
  const parentConversationId = systemId ?? '';
104
97
 
105
- const queue = new PageQueue(parentConversationId, 5);
98
+ const queue = new PageQueue(parentConversationId, '', 5);
106
99
  onRequestAborted(req, res, () => {
107
100
  queue.cancel();
108
101
  });
@@ -138,6 +131,7 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
138
131
  sendDone(res);
139
132
  } catch (err: any) {
140
133
  sendError(err, res);
134
+ } finally {
141
135
  if (!res.closed) {
142
136
  res.end();
143
137
  }
@@ -180,11 +174,17 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
180
174
  const promises: { [key: string]: Promise<void> } = {};
181
175
  const pageEventPromises: Promise<void>[] = [];
182
176
  const systemId = landingPagesStream.getConversationId();
183
- const pageQueue = new PageQueue(systemId, 5);
177
+ const systemPrompt = createFuture<string>();
178
+ if (aiRequest.skipImprovement) {
179
+ systemPrompt.resolve(aiRequest.prompt);
180
+ }
184
181
 
185
182
  landingPagesStream.on('data', async (data: StormEvent) => {
186
183
  try {
187
184
  sendEvent(res, data);
185
+ if (data.type === 'PROMPT_IMPROVE') {
186
+ systemPrompt.resolve(data.payload.prompt);
187
+ }
188
188
  if (data.type !== 'LANDING_PAGE') {
189
189
  return;
190
190
  }
@@ -218,7 +218,13 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
218
218
 
219
219
  UI_SERVERS[systemId] = new UIServer(systemId);
220
220
  await UI_SERVERS[systemId].start();
221
+ waitForStormStream(landingPagesStream).then(() => {
222
+ if (!systemPrompt.isResolved()) {
223
+ systemPrompt.resolve(aiRequest.prompt);
224
+ }
225
+ });
221
226
 
227
+ const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
222
228
  onRequestAborted(req, res, () => {
223
229
  pageQueue.cancel();
224
230
  });
@@ -281,9 +287,14 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
281
287
 
282
288
  const uniqueUserJourneyScreens: Record<string, UserJourneyScreen> = {};
283
289
 
290
+ let systemPrompt = aiRequest.prompt;
291
+
284
292
  userJourneysStream.on('data', (data: StormEvent) => {
285
293
  try {
286
294
  sendEvent(res, data);
295
+ if (data.type === 'PROMPT_IMPROVE') {
296
+ systemPrompt = data.payload.prompt;
297
+ }
287
298
  if (data.type !== 'USER_JOURNEY') {
288
299
  return;
289
300
  }
@@ -316,7 +327,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
316
327
  });
317
328
 
318
329
  themeStream.on('data', (evt) => {
319
- sendEvent(res, evt);
320
330
  if (evt.type === 'FILE_DONE') {
321
331
  theme = evt.payload.content;
322
332
  writeAssetToDisk(outerConversationId, evt).catch((err) => {
@@ -354,7 +364,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
354
364
  // Get the UI shells
355
365
  const shellsStream = await stormClient.createUIShells(
356
366
  {
357
- theme: theme ? `// filename: theme.css\n${theme}` : undefined,
367
+ theme: theme || undefined,
358
368
  pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
359
369
  name: screen.name,
360
370
  title: screen.title,
@@ -371,7 +381,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
371
381
  shellsStream.abort();
372
382
  });
373
383
 
374
- const queue = new PageQueue(outerConversationId, 5);
384
+ const queue = new PageQueue(outerConversationId, systemPrompt, 5);
375
385
  queue.setUiTheme(theme);
376
386
 
377
387
  shellsStream.on('data', (data: StormEvent) => {
@@ -410,8 +420,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
410
420
  created: Date.now(),
411
421
  });
412
422
 
413
- // Get the pages (5 at a time)
414
-
415
423
  const pagePromises: Promise<void>[] = [];
416
424
  onRequestAborted(req, res, () => {
417
425
  queue.cancel();
@@ -454,17 +462,18 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
454
462
  );
455
463
  }
456
464
 
457
- await queue.wait();
458
- await Promise.allSettled(pagePromises);
459
- await Promise.allSettled(pageEventPromises);
460
-
461
465
  if (userJourneysStream.isAborted()) {
462
466
  return;
463
467
  }
464
468
 
469
+ await queue.wait();
470
+ await Promise.allSettled(pagePromises);
471
+ await Promise.allSettled(pageEventPromises);
472
+
465
473
  sendDone(res);
466
474
  } catch (err) {
467
475
  sendError(err as Error, res);
476
+ } finally {
468
477
  if (!res.closed) {
469
478
  res.end();
470
479
  }
@@ -478,7 +487,8 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
478
487
 
479
488
  const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
480
489
  const storagePrefix = systemId ? systemId + '_' : 'mock_';
481
- const queue = new PageQueue(storagePrefix, 5);
490
+
491
+ const queue = new PageQueue(systemId!, '', 5);
482
492
 
483
493
  onRequestAborted(req, res, () => {
484
494
  queue.cancel();
@@ -498,8 +508,15 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
498
508
  }
499
509
  });
500
510
 
511
+ const pages = aiRequest.prompt.pages.filter((page) => page.conversationId);
512
+ if (pages.length === 0) {
513
+ console.log('No pages to update', aiRequest.prompt.pages);
514
+ sendDone(res);
515
+ return;
516
+ }
517
+
501
518
  await Promise.allSettled(
502
- aiRequest.prompt.pages.map((page) => {
519
+ pages.map((page) => {
503
520
  if (page.conversationId) {
504
521
  return queue.addPrompt(
505
522
  {
@@ -525,6 +542,7 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
525
542
  sendDone(res);
526
543
  } catch (err: any) {
527
544
  sendError(err as Error, res);
545
+ } finally {
528
546
  if (!res.closed) {
529
547
  res.end();
530
548
  }
@@ -535,14 +553,23 @@ router.post('/ui/vote', async (req: KapetaBodyRequest, res: Response) => {
535
553
  const conversationId = (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || '';
536
554
  const aiRequest: UIPageVoteRequest = JSON.parse(req.stringBody ?? '{}');
537
555
  const { topic, vote, mainConversationId } = aiRequest;
538
- return stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
556
+ try {
557
+ await stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
558
+ } catch (e: any) {
559
+ res.status(500).send({ error: e.message });
560
+ }
539
561
  });
540
562
 
541
563
  router.post('/ui/get-vote', async (req: KapetaBodyRequest, res: Response) => {
542
564
  const conversationId = (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || '';
543
565
  const aiRequest: UIPageGetVoteRequest = JSON.parse(req.stringBody ?? '{}');
544
566
  const { topic, mainConversationId } = aiRequest;
545
- return stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
567
+ try {
568
+ const vote = await stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
569
+ res.send({ vote });
570
+ } catch (e: any) {
571
+ res.status(500).send({ error: e.message });
572
+ }
546
573
  });
547
574
 
548
575
  router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
@@ -689,7 +716,6 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
689
716
 
690
717
  router.post('/block/codegen', async (req: KapetaBodyRequest, res: Response) => {
691
718
  const body: StormCodegenRequest = JSON.parse(req.stringBody ?? '{}');
692
- console.log('Codegen request', body);
693
719
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
694
720
  try {
695
721
  const stormCodegen = new StormCodegen(conversationId ?? '', body.prompt, [body.block], body.events || []);
@@ -791,11 +817,14 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
791
817
  }
792
818
 
793
819
  async function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
794
- try {
795
- await writePageToDisk(mainConversationId, data);
796
- } catch (err) {
797
- console.error('Failed to write page to disk', err);
820
+ if (data.payload.content) {
821
+ try {
822
+ await writePageToDisk(mainConversationId, data);
823
+ } catch (err) {
824
+ console.error('Failed to write page to disk', err);
825
+ }
798
826
  }
827
+
799
828
  sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
800
829
  }
801
830
 
@@ -43,7 +43,7 @@ export interface UIPagePrompt {
43
43
  description: string;
44
44
  storage_prefix: string;
45
45
  shell_page?: string;
46
- // contents of theme.css
46
+ // contents of theme.md
47
47
  theme?: string;
48
48
  }
49
49