@kapeta/local-cluster-service 0.67.2 → 0.67.3

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.
@@ -16,12 +16,16 @@ const page_utils_1 = require("./page-utils");
16
16
  class PageQueue extends node_events_1.EventEmitter {
17
17
  queue;
18
18
  systemId;
19
+ systemPrompt;
19
20
  references = new Map();
21
+ pages = new Map();
22
+ images = new Map();
20
23
  uiShells = [];
21
24
  theme = '';
22
- constructor(systemId, concurrency = 5) {
25
+ constructor(systemId, systemPrompt, concurrency = 5) {
23
26
  super();
24
27
  this.systemId = systemId;
28
+ this.systemPrompt = systemPrompt;
25
29
  this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
26
30
  }
27
31
  on(event, listener) {
@@ -36,68 +40,128 @@ class PageQueue extends node_events_1.EventEmitter {
36
40
  setUiTheme(theme) {
37
41
  this.theme = theme;
38
42
  }
39
- addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
40
- if (!overwrite && this.references.has(initialPrompt.path)) {
43
+ hasPrompt(path) {
44
+ if (this.references.has(path)) {
41
45
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
42
- return Promise.resolve();
46
+ return true;
43
47
  }
44
- if (!overwrite && (0, page_utils_1.hasPageOnDisk)(this.systemId, initialPrompt.method, initialPrompt.path)) {
48
+ if ((0, page_utils_1.hasPageOnDisk)(this.systemId, 'GET', path)) {
45
49
  //console.log('Ignoring prompt with existing page', initialPrompt.path);
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
55
+ if (!overwrite && this.hasPrompt(initialPrompt.path)) {
56
+ //console.log('Ignoring duplicate prompt', initialPrompt.path);
46
57
  return Promise.resolve();
47
58
  }
48
59
  const prompt = {
49
60
  ...initialPrompt,
50
61
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
62
+ prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
51
63
  theme: this.theme,
52
64
  };
53
65
  const generator = new PageGenerator(prompt, conversationId);
54
66
  this.references.set(prompt.path, generator);
67
+ this.pages.set(prompt.path, prompt.description);
55
68
  return this.addPageGenerator(generator);
56
69
  }
57
- async addPageGenerator(generator) {
58
- generator.on('event', (event) => this.emit('event', event));
59
- generator.on('page_refs', async ({ event, references }) => {
60
- const promises = references.map(async (reference) => {
61
- if (reference.url.startsWith('#') ||
62
- reference.url.startsWith('javascript:') ||
63
- reference.url.startsWith('http://') ||
64
- reference.url.startsWith('https://')) {
70
+ getPrefix() {
71
+ let promptPrefix = '';
72
+ if (this.systemPrompt) {
73
+ promptPrefix = `For a system with this description: ${this.systemPrompt}\n`;
74
+ }
75
+ return promptPrefix;
76
+ }
77
+ wrapPagePrompt(pagePath, prompt) {
78
+ let promptPrefix = this.getPrefix();
79
+ let promptPostfix = '';
80
+ if (this.pages.size > 0) {
81
+ promptPostfix = `\nThe following pages are already implemented:\n`;
82
+ this.pages.forEach((description, path) => {
83
+ if (pagePath === path) {
65
84
  return;
66
85
  }
67
- switch (reference.type) {
68
- case 'image':
69
- await this.addImagePrompt({
70
- ...reference,
71
- content: event.payload.content,
72
- });
73
- break;
74
- case 'css':
75
- case 'javascript':
76
- //console.log('Ignoring reference', reference);
77
- break;
78
- case 'html':
79
- //console.log('Adding page generator for', reference);
80
- const paths = Array.from(this.references.keys());
81
- this.addPrompt({
82
- name: reference.name,
83
- title: reference.title,
84
- path: reference.url,
85
- method: 'GET',
86
- storage_prefix: this.systemId + '_',
87
- prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
88
- `The page was referenced from this page: \`\`\`html\n${event.payload.content}\n\`\`\`\n` +
89
- (paths.length > 0
90
- ? `\nThese paths are already implemented:\n- ${paths.join('\n - ')}\n\n`
91
- : ''),
92
- description: reference.description,
93
- filename: '',
94
- theme: this.theme,
95
- });
96
- break;
97
- }
86
+ promptPostfix += `- PAGE: '${path}' -> ${description}.\n`;
87
+ });
88
+ }
89
+ if (this.images.size > 0) {
90
+ promptPostfix += `\nThe following images already exist:\n`;
91
+ this.images.forEach((description, path) => {
92
+ promptPostfix += `- IMAGE: '${path}' -> ${description}.\n`;
98
93
  });
99
- await Promise.allSettled(promises);
100
- this.emit('page', event);
94
+ }
95
+ return promptPrefix + prompt + promptPostfix;
96
+ }
97
+ async addPageGenerator(generator) {
98
+ generator.on('event', (event) => this.emit('event', event));
99
+ generator.on('page_refs', async ({ event, references, future }) => {
100
+ try {
101
+ const initialPrompts = [];
102
+ let promises = references.map(async (reference) => {
103
+ if (reference.url.startsWith('#') ||
104
+ reference.url.startsWith('javascript:') ||
105
+ reference.url.startsWith('http://') ||
106
+ reference.url.startsWith('https://')) {
107
+ return;
108
+ }
109
+ switch (reference.type) {
110
+ case 'image':
111
+ await this.addImagePrompt({
112
+ ...reference,
113
+ content: event.payload.content,
114
+ });
115
+ break;
116
+ case 'css':
117
+ case 'javascript':
118
+ //console.log('Ignoring reference', reference);
119
+ break;
120
+ case 'html':
121
+ //console.log('Adding page generator for', reference);
122
+ this.pages.set(reference.url, reference.description);
123
+ initialPrompts.push({
124
+ name: reference.name,
125
+ title: reference.title,
126
+ path: reference.url,
127
+ method: 'GET',
128
+ storage_prefix: this.systemId + '_',
129
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
130
+ `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
131
+ description: reference.description,
132
+ filename: '',
133
+ theme: this.theme,
134
+ });
135
+ break;
136
+ }
137
+ });
138
+ await Promise.allSettled(promises);
139
+ initialPrompts.forEach((prompt) => {
140
+ if (!this.hasPrompt(prompt.path)) {
141
+ this.emit('page', {
142
+ type: 'PAGE',
143
+ reason: 'reference',
144
+ created: Date.now(),
145
+ payload: {
146
+ name: prompt.name,
147
+ title: prompt.title,
148
+ filename: '',
149
+ method: 'GET',
150
+ path: prompt.path,
151
+ prompt: prompt.description,
152
+ conversationId: '',
153
+ content: '',
154
+ description: prompt.description,
155
+ },
156
+ });
157
+ }
158
+ this.addPrompt(prompt);
159
+ });
160
+ this.emit('page', event);
161
+ }
162
+ finally {
163
+ future.resolve();
164
+ }
101
165
  });
102
166
  return this.queue.add(() => generator.generate());
103
167
  }
@@ -108,7 +172,14 @@ class PageQueue extends node_events_1.EventEmitter {
108
172
  return this.queue.wait();
109
173
  }
110
174
  async addImagePrompt(prompt) {
111
- const result = await stormClient_1.stormClient.createImage(`Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
175
+ if (this.images.has(prompt.url)) {
176
+ //console.log('Ignoring duplicate image prompt', prompt);
177
+ return;
178
+ }
179
+ this.images.set(prompt.url, prompt.description);
180
+ const prefix = this.getPrefix();
181
+ const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
182
+ //console.log('Adding image prompt', prompt);
112
183
  const futures = [];
113
184
  result.on('data', async (event) => {
114
185
  if (event.type === 'IMAGE') {
@@ -144,27 +215,29 @@ class PageGenerator extends node_events_1.EventEmitter {
144
215
  }
145
216
  async generate() {
146
217
  return new Promise(async (resolve) => {
147
- const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
148
218
  const promises = [];
219
+ const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
149
220
  screenStream.on('data', (event) => {
150
221
  if (event.type === 'PAGE') {
151
222
  event.payload.conversationId = this.conversationId;
152
223
  promises.push((async () => {
153
224
  const references = await this.resolveReferences(event.payload.content);
154
225
  //console.log('Resolved references for page', references, event.payload);
226
+ const future = (0, PromiseQueue_1.createFuture)();
155
227
  this.emit('page_refs', {
156
228
  event,
157
229
  references,
230
+ future,
158
231
  });
232
+ await future.promise;
159
233
  })());
160
234
  return;
161
235
  }
162
236
  this.emit('event', event);
163
237
  });
164
- screenStream.on('end', () => {
165
- Promise.allSettled(promises).finally(resolve);
166
- });
167
238
  await screenStream.waitForDone();
239
+ await Promise.all(promises);
240
+ resolve();
168
241
  });
169
242
  }
170
243
  async resolveReferences(content) {
@@ -23,6 +23,7 @@ const page_utils_1 = require("./page-utils");
23
23
  const UIServer_1 = require("./UIServer");
24
24
  const crypto_1 = require("crypto");
25
25
  const PageGenerator_1 = require("./PageGenerator");
26
+ const PromiseQueue_1 = require("./PromiseQueue");
26
27
  const UI_SERVERS = {};
27
28
  const router = (0, express_promise_router_1.default)();
28
29
  router.use('/', cors_1.corsHandler);
@@ -47,7 +48,7 @@ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
47
48
  description: screenData.payload.description,
48
49
  prompt: screenData.payload.prompt,
49
50
  path: screenData.payload.path,
50
- url: server ? server.resolveUrl(screenData) : '',
51
+ url: server && screenData.payload.content ? server.resolveUrl(screenData) : '',
51
52
  method: screenData.payload.method,
52
53
  conversationId: innerConversationId,
53
54
  },
@@ -68,7 +69,7 @@ router.post('/ui/screen', async (req, res) => {
68
69
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
69
70
  res.set(stormClient_1.ConversationIdHeader, conversationId);
70
71
  const parentConversationId = systemId ?? '';
71
- const queue = new PageGenerator_1.PageQueue(parentConversationId, 5);
72
+ const queue = new PageGenerator_1.PageQueue(parentConversationId, '', 5);
72
73
  onRequestAborted(req, res, () => {
73
74
  queue.cancel();
74
75
  });
@@ -100,6 +101,8 @@ router.post('/ui/screen', async (req, res) => {
100
101
  }
101
102
  catch (err) {
102
103
  sendError(err, res);
104
+ }
105
+ finally {
103
106
  if (!res.closed) {
104
107
  res.end();
105
108
  }
@@ -133,10 +136,16 @@ router.post('/:handle/ui/iterative', async (req, res) => {
133
136
  const promises = {};
134
137
  const pageEventPromises = [];
135
138
  const systemId = landingPagesStream.getConversationId();
136
- const pageQueue = new PageGenerator_1.PageQueue(systemId, 5);
139
+ const systemPrompt = (0, PromiseQueue_1.createFuture)();
140
+ if (aiRequest.skipImprovement) {
141
+ systemPrompt.resolve(aiRequest.prompt);
142
+ }
137
143
  landingPagesStream.on('data', async (data) => {
138
144
  try {
139
145
  sendEvent(res, data);
146
+ if (data.type === 'PROMPT_IMPROVE') {
147
+ systemPrompt.resolve(data.payload.prompt);
148
+ }
140
149
  if (data.type !== 'LANDING_PAGE') {
141
150
  return;
142
151
  }
@@ -168,6 +177,12 @@ router.post('/:handle/ui/iterative', async (req, res) => {
168
177
  });
169
178
  UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
170
179
  await UI_SERVERS[systemId].start();
180
+ waitForStormStream(landingPagesStream).then(() => {
181
+ if (!systemPrompt.isResolved()) {
182
+ systemPrompt.resolve(aiRequest.prompt);
183
+ }
184
+ });
185
+ const pageQueue = new PageGenerator_1.PageQueue(systemId, await systemPrompt.promise, 5);
171
186
  onRequestAborted(req, res, () => {
172
187
  pageQueue.cancel();
173
188
  });
@@ -218,9 +233,13 @@ router.post('/:handle/ui', async (req, res) => {
218
233
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
219
234
  res.set(stormClient_1.ConversationIdHeader, outerConversationId);
220
235
  const uniqueUserJourneyScreens = {};
236
+ let systemPrompt = aiRequest.prompt;
221
237
  userJourneysStream.on('data', (data) => {
222
238
  try {
223
239
  sendEvent(res, data);
240
+ if (data.type === 'PROMPT_IMPROVE') {
241
+ systemPrompt = data.payload.prompt;
242
+ }
224
243
  if (data.type !== 'USER_JOURNEY') {
225
244
  return;
226
245
  }
@@ -249,7 +268,6 @@ router.post('/:handle/ui', async (req, res) => {
249
268
  themeStream.abort();
250
269
  });
251
270
  themeStream.on('data', (evt) => {
252
- sendEvent(res, evt);
253
271
  if (evt.type === 'FILE_DONE') {
254
272
  theme = evt.payload.content;
255
273
  (0, page_utils_1.writeAssetToDisk)(outerConversationId, evt).catch((err) => {
@@ -298,7 +316,7 @@ router.post('/:handle/ui', async (req, res) => {
298
316
  onRequestAborted(req, res, () => {
299
317
  shellsStream.abort();
300
318
  });
301
- const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
319
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, systemPrompt, 5);
302
320
  queue.setUiTheme(theme);
303
321
  shellsStream.on('data', (data) => {
304
322
  //console.log('Processing shell event', data);
@@ -328,7 +346,6 @@ router.post('/:handle/ui', async (req, res) => {
328
346
  },
329
347
  created: Date.now(),
330
348
  });
331
- // Get the pages (5 at a time)
332
349
  const pagePromises = [];
333
350
  onRequestAborted(req, res, () => {
334
351
  queue.cancel();
@@ -365,16 +382,18 @@ router.post('/:handle/ui', async (req, res) => {
365
382
  theme,
366
383
  }));
367
384
  }
368
- await queue.wait();
369
- await Promise.allSettled(pagePromises);
370
- await Promise.allSettled(pageEventPromises);
371
385
  if (userJourneysStream.isAborted()) {
372
386
  return;
373
387
  }
388
+ await queue.wait();
389
+ await Promise.allSettled(pagePromises);
390
+ await Promise.allSettled(pageEventPromises);
374
391
  sendDone(res);
375
392
  }
376
393
  catch (err) {
377
394
  sendError(err, res);
395
+ }
396
+ finally {
378
397
  if (!res.closed) {
379
398
  res.end();
380
399
  }
@@ -386,7 +405,7 @@ router.post('/ui/edit', async (req, res) => {
386
405
  req.headers[stormClient_1.ConversationIdHeader.toLowerCase()]);
387
406
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
388
407
  const storagePrefix = systemId ? systemId + '_' : 'mock_';
389
- const queue = new PageGenerator_1.PageQueue(storagePrefix, 5);
408
+ const queue = new PageGenerator_1.PageQueue(systemId, '', 5);
390
409
  onRequestAborted(req, res, () => {
391
410
  queue.cancel();
392
411
  });
@@ -401,7 +420,13 @@ router.post('/ui/edit', async (req, res) => {
401
420
  sendEvent(res, data);
402
421
  }
403
422
  });
404
- await Promise.allSettled(aiRequest.prompt.pages.map((page) => {
423
+ const pages = aiRequest.prompt.pages.filter((page) => page.conversationId);
424
+ if (pages.length === 0) {
425
+ console.log('No pages to update', aiRequest.prompt.pages);
426
+ sendDone(res);
427
+ return;
428
+ }
429
+ await Promise.allSettled(pages.map((page) => {
405
430
  if (page.conversationId) {
406
431
  return queue.addPrompt({
407
432
  title: page.title,
@@ -421,6 +446,8 @@ router.post('/ui/edit', async (req, res) => {
421
446
  }
422
447
  catch (err) {
423
448
  sendError(err, res);
449
+ }
450
+ finally {
424
451
  if (!res.closed) {
425
452
  res.end();
426
453
  }
@@ -430,13 +457,24 @@ router.post('/ui/vote', async (req, res) => {
430
457
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
431
458
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
432
459
  const { topic, vote, mainConversationId } = aiRequest;
433
- return stormClient_1.stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
460
+ try {
461
+ await stormClient_1.stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
462
+ }
463
+ catch (e) {
464
+ res.status(500).send({ error: e.message });
465
+ }
434
466
  });
435
467
  router.post('/ui/get-vote', async (req, res) => {
436
468
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
437
469
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
438
470
  const { topic, mainConversationId } = aiRequest;
439
- return stormClient_1.stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
471
+ try {
472
+ const vote = await stormClient_1.stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
473
+ res.send({ vote });
474
+ }
475
+ catch (e) {
476
+ res.status(500).send({ error: e.message });
477
+ }
440
478
  });
441
479
  router.post('/:handle/all', async (req, res) => {
442
480
  const handle = req.params.handle;
@@ -556,7 +594,6 @@ router.post('/block/create', async (req, res) => {
556
594
  });
557
595
  router.post('/block/codegen', async (req, res) => {
558
596
  const body = JSON.parse(req.stringBody ?? '{}');
559
- console.log('Codegen request', body);
560
597
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
561
598
  try {
562
599
  const stormCodegen = new codegen_1.StormCodegen(conversationId ?? '', body.prompt, [body.block], body.events || []);
@@ -642,11 +679,13 @@ function onRequestAborted(req, res, onAborted) {
642
679
  });
643
680
  }
644
681
  async function sendPageEvent(mainConversationId, data, res) {
645
- try {
646
- await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
647
- }
648
- catch (err) {
649
- console.error('Failed to write page to disk', err);
682
+ if (data.payload.content) {
683
+ try {
684
+ await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
685
+ }
686
+ catch (err) {
687
+ console.error('Failed to write page to disk', err);
688
+ }
650
689
  }
651
690
  sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
652
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.67.2",
3
+ "version": "0.67.3",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {