@kapeta/local-cluster-service 0.70.2 → 0.70.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.
@@ -7,8 +7,9 @@ import uuid from 'node-uuid';
7
7
  import { stormClient, UIPagePrompt } from './stormClient';
8
8
  import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
9
9
  import { EventEmitter } from 'node:events';
10
- import { PromiseQueue } from './PromiseQueue';
11
- import { hasPageOnDisk } from './page-utils';
10
+ import PQueue from 'p-queue';
11
+
12
+ import { hasPageOnDisk, normalizePath } from './page-utils';
12
13
 
13
14
  export interface ImagePrompt {
14
15
  name: string;
@@ -23,11 +24,11 @@ export interface ImagePrompt {
23
24
  type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & { shellType?: 'public' | 'admin' | 'user' };
24
25
 
25
26
  export class PageQueue extends EventEmitter {
26
- private readonly queue: PromiseQueue;
27
- private readonly eventQueue: PromiseQueue;
27
+ private readonly queue: PQueue;
28
+ private readonly eventQueue: PQueue;
28
29
  private readonly systemId: string;
29
30
  private readonly systemPrompt: string;
30
- private readonly references: Map<string, PageGenerator> = new Map();
31
+ private readonly references: Map<string, boolean> = new Map();
31
32
  private readonly pages: Map<string, string> = new Map();
32
33
  private readonly images: Map<string, string> = new Map();
33
34
  private uiShells: UIShell[] = [];
@@ -37,8 +38,8 @@ export class PageQueue extends EventEmitter {
37
38
  super();
38
39
  this.systemId = systemId;
39
40
  this.systemPrompt = systemPrompt;
40
- this.queue = new PromiseQueue(concurrency);
41
- this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
41
+ this.queue = new PQueue({ concurrency });
42
+ this.eventQueue = new PQueue({ concurrency: Number.MAX_VALUE });
42
43
  }
43
44
 
44
45
  on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
@@ -94,11 +95,10 @@ export class PageQueue extends EventEmitter {
94
95
  theme: this.theme,
95
96
  };
96
97
 
97
- const generator = new PageGenerator(prompt, conversationId);
98
- this.references.set(prompt.path, generator);
98
+ this.references.set(prompt.path, true);
99
99
  this.pages.set(prompt.path, prompt.description);
100
100
 
101
- return this.addPageGenerator(generator);
101
+ return this.queue.add<void>(() => this.generate(prompt, conversationId));
102
102
  }
103
103
  private getPrefix(): string {
104
104
  let promptPrefix = '';
@@ -132,111 +132,110 @@ export class PageQueue extends EventEmitter {
132
132
  return promptPrefix + prompt + promptPostfix;
133
133
  }
134
134
 
135
- private async addPageGenerator(generator: PageGenerator) {
136
- generator.on('event', (event: StormEvent) => this.emit('event', event));
137
- generator.on('page_refs', async ({ event, references }) => {
138
- try {
139
- const matchesExistingPages = (url: string) => {
140
- return [...this.pages.keys()].some((path) => new RegExp(path.replaceAll('/*', '/[^/]+')).test(url));
141
- };
142
- const initialPrompts: InitialPrompt[] = [];
143
- const resourcePromises = references.map(async (reference) => {
144
- if (
145
- reference.url.startsWith('#') ||
146
- reference.url.startsWith('javascript:') ||
147
- reference.url.startsWith('http://') ||
148
- reference.url.startsWith('https://')
149
- ) {
150
- return;
151
- }
135
+ private async processPageEventWithReferences(event: StormEventPage) {
136
+ try {
137
+ console.log('Processing page event', event.payload.path);
138
+ const references = await this.resolveReferences(event.payload.content);
139
+ const matchesExistingPages = (url: string) => {
140
+ return [...this.pages.keys()].some((path) =>
141
+ new RegExp(`^${path.replaceAll('/*', '/[^/]+')}$`).test(url)
142
+ );
143
+ };
144
+ const initialPrompts: InitialPrompt[] = [];
145
+ const resourcePromises = references.map(async (reference) => {
146
+ if (
147
+ reference.url.startsWith('#') ||
148
+ reference.url.startsWith('javascript:') ||
149
+ reference.url.startsWith('http://') ||
150
+ reference.url.startsWith('https://') ||
151
+ reference.url.startsWith('data:') ||
152
+ reference.url.startsWith('blob:') ||
153
+ reference.url.startsWith('mailto:')
154
+ ) {
155
+ return;
156
+ }
152
157
 
153
- switch (reference.type) {
154
- case 'image':
155
- await this.addImagePrompt({
156
- ...reference,
157
- content: event.payload.content,
158
- });
159
- break;
160
- case 'css':
161
- case 'javascript':
162
- //console.log('Ignoring reference', reference);
163
- break;
164
- case 'html':
165
- //console.log('Adding page generator for', reference);
166
- if (matchesExistingPages(reference.url)) {
167
- break;
168
- }
169
- this.pages.set(reference.url, reference.description);
170
-
171
- initialPrompts.push({
172
- name: reference.name,
173
- title: reference.title,
174
- path: reference.url,
175
- method: 'GET',
176
- storage_prefix: this.systemId + '_',
177
- prompt:
178
- `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
179
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
180
- description: reference.description,
181
- // Only used for matching
182
- filename: reference.name + '.ref.html',
183
- theme: this.theme,
184
- });
158
+ switch (reference.type) {
159
+ case 'image':
160
+ await this.addImagePrompt({
161
+ ...reference,
162
+ content: event.payload.content,
163
+ });
164
+ break;
165
+ case 'css':
166
+ case 'javascript':
167
+ //console.log('Ignoring reference', reference);
168
+ break;
169
+ case 'html': {
170
+ const normalizedPath = normalizePath(reference.url);
171
+ if (matchesExistingPages(normalizedPath)) {
185
172
  break;
186
- }
187
- });
188
-
189
- // Wait for resources to be generated
190
- await Promise.allSettled(resourcePromises);
191
- this.emit('page', event);
192
-
193
- // Emit any new pages after the current page to increase responsiveness
194
- const newPages = initialPrompts.map((prompt) => {
195
- if (!this.hasPrompt(prompt.path)) {
196
- this.emit('page', {
197
- type: 'PAGE',
198
- reason: 'reference',
199
- created: Date.now(),
200
- payload: {
201
- name: prompt.name,
202
- title: prompt.title,
203
- filename: prompt.filename,
204
- method: 'GET',
205
- path: prompt.path,
206
- prompt: prompt.description,
207
- conversationId: '',
208
- content: '',
209
- description: prompt.description,
210
- },
173
+ }
174
+ this.pages.set(normalizedPath, reference.description);
175
+
176
+ initialPrompts.push({
177
+ name: reference.name,
178
+ title: reference.title,
179
+ path: normalizedPath,
180
+ method: 'GET',
181
+ storage_prefix: this.systemId + '_',
182
+ prompt:
183
+ `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
184
+ `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
185
+ description: reference.description,
186
+ // Only used for matching
187
+ filename: reference.name + '.ref.html',
188
+ theme: this.theme,
211
189
  });
190
+ break;
212
191
  }
213
- return this.addPrompt(prompt);
214
- });
215
- await Promise.allSettled(newPages);
216
- } catch (e) {
217
- console.error('Failed to process event', e);
218
- throw e;
219
- }
220
- });
221
- return this.queue.add(() => generator.generate());
192
+ }
193
+ });
194
+
195
+ // Wait for resources to be generated
196
+ await Promise.allSettled(resourcePromises);
197
+ this.emit('page', event);
198
+
199
+ // Emit any new pages after the current page to increase responsiveness
200
+ void initialPrompts.map((prompt) => {
201
+ if (!this.hasPrompt(prompt.path)) {
202
+ this.emit('page', {
203
+ type: 'PAGE',
204
+ reason: 'reference',
205
+ created: Date.now(),
206
+ payload: {
207
+ name: prompt.name,
208
+ title: prompt.title,
209
+ filename: prompt.filename,
210
+ method: 'GET',
211
+ path: prompt.path,
212
+ prompt: prompt.description,
213
+ conversationId: '',
214
+ content: '',
215
+ description: prompt.description,
216
+ },
217
+ });
218
+ }
219
+ return this.addPrompt(prompt);
220
+ });
221
+ } catch (e) {
222
+ console.error('Failed to process event', e);
223
+ throw e;
224
+ }
222
225
  }
223
226
 
224
227
  public cancel() {
225
- this.queue.cancel();
226
- this.eventQueue.cancel();
228
+ this.queue.clear();
229
+ this.eventQueue.clear();
227
230
  }
228
231
 
229
232
  public async wait() {
230
- while (!this.eventQueue.empty || !this.queue.empty) {
231
- await this.eventQueue.wait();
232
- await this.queue.wait();
233
+ while (this.eventQueue.size || this.eventQueue.pending || this.queue.size || this.queue.pending) {
234
+ await this.eventQueue.onIdle();
235
+ await this.queue.onIdle();
233
236
  }
234
237
  }
235
238
 
236
- public get length() {
237
- return this.queue.length + this.eventQueue.length;
238
- }
239
-
240
239
  private async addImagePrompt(prompt: ImagePrompt) {
241
240
  if (this.images.has(prompt.url)) {
242
241
  //console.log('Ignoring duplicate image prompt', prompt);
@@ -257,56 +256,16 @@ export class PageQueue extends EventEmitter {
257
256
 
258
257
  await result.waitForDone();
259
258
  }
260
- }
261
-
262
- export class PageGenerator extends EventEmitter {
263
- private readonly eventQueue: PromiseQueue;
264
- private readonly conversationId: string;
265
- public readonly prompt: UIPagePrompt;
266
-
267
- constructor(prompt: UIPagePrompt, conversationId: string = uuid.v4()) {
268
- super();
269
- this.conversationId = conversationId;
270
- this.prompt = prompt;
271
- this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
272
- }
273
259
 
274
- on(event: 'event', listener: (data: StormEvent) => void): this;
275
- on(
276
- event: 'page_refs',
277
- listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => Promise<void>
278
- ): this;
279
-
280
- on(event: string, listener: (...args: any[]) => void): this {
281
- return super.on(event, (...args) => {
282
- void this.eventQueue.add(async () => listener(...args));
283
- });
284
- }
285
-
286
- emit(type: 'event', event: StormEvent): boolean;
287
- emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
288
- emit(eventName: string | symbol, ...args: any[]): boolean {
289
- return super.emit(eventName, ...args);
290
- }
291
-
292
- public async generate() {
260
+ public async generate(prompt: UIPagePrompt, conversationId: string) {
293
261
  const promises: Promise<void>[] = [];
294
- const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
262
+ const screenStream = await stormClient.createUIPage(prompt, conversationId);
295
263
 
296
264
  screenStream.on('data', (event: StormEvent) => {
297
265
  if (event.type === 'PAGE') {
298
- event.payload.conversationId = this.conversationId;
299
-
300
- promises.push(
301
- (async () => {
302
- const references = await this.resolveReferences(event.payload.content);
303
- //console.log('Resolved references for page', references, event.payload);
304
- this.emit('page_refs', {
305
- event,
306
- references,
307
- });
308
- })()
309
- );
266
+ event.payload.conversationId = conversationId;
267
+
268
+ promises.push(this.processPageEventWithReferences(event));
310
269
  return;
311
270
  }
312
271
 
@@ -315,7 +274,6 @@ export class PageGenerator extends EventEmitter {
315
274
 
316
275
  await screenStream.waitForDone();
317
276
  await Promise.all(promises);
318
- await this.eventQueue.wait();
319
277
  }
320
278
 
321
279
  private async resolveReferences(content: string) {
@@ -15,7 +15,7 @@ import { ImagePrompt } from './PageGenerator';
15
15
 
16
16
  export const SystemIdHeader = 'System-Id';
17
17
 
18
- function normalizePath(path: string) {
18
+ export function normalizePath(path: string) {
19
19
  return path
20
20
  .replace(/\?.*$/gi, '')
21
21
  .replace(/:[a-z][a-z_]*\b/gi, '*')
@@ -78,7 +78,8 @@ export function getSystemBaseDir(systemId: string) {
78
78
  }
79
79
 
80
80
  function getFilePath(method: string) {
81
- return Path.join(method.toLowerCase(), 'index.html');
81
+ // For HEAD requests, we assume we're serving looking for a GET resource
82
+ return Path.join(method === 'HEAD' ? 'get' : method.toLowerCase(), 'index.html');
82
83
  }
83
84
 
84
85
  export function resolveReadPath(systemId: string, path: string, method: string) {
@@ -105,7 +106,7 @@ export function resolveReadPath(systemId: string, path: string, method: string)
105
106
  return fullPath;
106
107
  }
107
108
 
108
- const parts = path.split(/\*/g);
109
+ const parts = path.split('/');
109
110
 
110
111
  let currentPath = '';
111
112
 
@@ -214,7 +215,7 @@ function getFallbackHtml(path: string, method: string): string {
214
215
  <script>
215
216
  const checkInterval = 3000;
216
217
  function checkPageReady() {
217
- fetch('${path}', { method: 'HEAD' })
218
+ fetch('/${path}', { method: 'HEAD' })
218
219
  .then(response => {
219
220
  if (response.status === 200) {
220
221
  // The page is ready, reload to fetch it
@@ -47,8 +47,7 @@ import {
47
47
  import { UIServer } from './UIServer';
48
48
  import { randomUUID } from 'crypto';
49
49
  import { ImagePrompt, PageQueue } from './PageGenerator';
50
- import { createFuture } from './PromiseQueue';
51
- import { copyDirectory } from './utils';
50
+ import { copyDirectory, createFuture } from './utils';
52
51
 
53
52
  const UI_SERVERS: { [key: string]: UIServer } = {};
54
53
  const router = Router();
@@ -250,9 +249,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
250
249
  UI_SERVERS[systemId] = new UIServer(systemId);
251
250
  await UI_SERVERS[systemId].start();
252
251
  waitForStormStream(landingPagesStream).then(() => {
253
- if (!systemPrompt.isResolved()) {
254
- systemPrompt.resolve(aiRequest.prompt);
255
- }
252
+ systemPrompt.resolve(aiRequest.prompt);
256
253
  });
257
254
 
258
255
  const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
@@ -5,7 +5,11 @@
5
5
  import FS from 'fs-extra';
6
6
  import Path from 'path';
7
7
 
8
- export async function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void> {
8
+ export async function copyDirectory(
9
+ src: string,
10
+ dest: string,
11
+ modifyHtml: (fileName: string, content: string) => Promise<string>
12
+ ): Promise<void> {
9
13
  await FS.promises.mkdir(dest, { recursive: true });
10
14
  const entries = await FS.promises.readdir(src, { withFileTypes: true });
11
15
 
@@ -26,3 +30,17 @@ export async function copyDirectory(src: string, dest: string, modifyHtml: (file
26
30
  }
27
31
  }
28
32
  }
33
+
34
+ export function createFuture<T = void>() {
35
+ let resolve: (value: T | PromiseLike<T>) => void = () => {};
36
+ let reject: (reason?: any) => void = () => {};
37
+ const promise = new Promise<T>((res, rej) => {
38
+ resolve = res;
39
+ reject = rej;
40
+ });
41
+ return {
42
+ promise,
43
+ resolve,
44
+ reject,
45
+ };
46
+ }
@@ -1,25 +0,0 @@
1
- /**
2
- * Copyright 2023 Kapeta Inc.
3
- * SPDX-License-Identifier: BUSL-1.1
4
- */
5
- export type Future<T> = () => Promise<T>;
6
- export type FuturePromise<T> = {
7
- promise: Promise<T>;
8
- resolve: (value: T) => void;
9
- reject: (reason: any) => void;
10
- isResolved: () => boolean;
11
- };
12
- export declare function createFuture<T = void>(): FuturePromise<T>;
13
- export declare class PromiseQueue {
14
- private readonly queue;
15
- private readonly active;
16
- private readonly maxConcurrency;
17
- constructor(maxConcurrency?: number);
18
- private toInternal;
19
- add<T>(future: Future<T>): Promise<T>;
20
- private next;
21
- cancel(): void;
22
- wait(): Promise<void>;
23
- get empty(): boolean;
24
- get length(): number;
25
- }
@@ -1,97 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromiseQueue = exports.createFuture = void 0;
4
- function createFuture() {
5
- let resolved = false;
6
- let resolve = () => {
7
- resolved = true;
8
- };
9
- let reject = () => {
10
- resolved = true;
11
- };
12
- const promise = new Promise((res, rej) => {
13
- resolve = (value) => {
14
- resolved = true;
15
- res(value);
16
- };
17
- reject = (reason) => {
18
- resolved = true;
19
- rej(reason);
20
- };
21
- });
22
- return {
23
- promise,
24
- resolve,
25
- reject,
26
- isResolved: () => resolved,
27
- };
28
- }
29
- exports.createFuture = createFuture;
30
- class PromiseQueue {
31
- queue = [];
32
- active = [];
33
- // Maximum number of concurrent promises
34
- maxConcurrency;
35
- constructor(maxConcurrency = 5) {
36
- this.maxConcurrency = maxConcurrency;
37
- }
38
- toInternal(future) {
39
- let resolve = () => { };
40
- let reject = () => { };
41
- const promise = new Promise((res, rej) => {
42
- resolve = res;
43
- reject = rej;
44
- });
45
- return {
46
- execute: future,
47
- promise,
48
- resolve,
49
- reject,
50
- };
51
- }
52
- add(future) {
53
- const internal = this.toInternal(future);
54
- this.queue.push(internal);
55
- this.next();
56
- return internal.promise;
57
- }
58
- next() {
59
- if (this.active.length >= this.maxConcurrency) {
60
- return false;
61
- }
62
- if (this.queue.length === 0) {
63
- return false;
64
- }
65
- const future = this.queue.shift();
66
- if (!future) {
67
- return false;
68
- }
69
- const promise = future
70
- .execute()
71
- .then(future.resolve)
72
- .catch(future.reject)
73
- .finally(() => {
74
- this.active.splice(this.active.indexOf(promise), 1);
75
- this.next();
76
- });
77
- this.active.push(promise);
78
- return this.next();
79
- }
80
- cancel() {
81
- this.queue.splice(0, this.queue.length);
82
- this.active.splice(0, this.active.length);
83
- }
84
- async wait() {
85
- while (this.queue.length > 0 || this.active.length > 0) {
86
- await Promise.allSettled(this.active);
87
- this.next();
88
- }
89
- }
90
- get empty() {
91
- return this.queue.length === 0 && this.active.length === 0;
92
- }
93
- get length() {
94
- return this.queue.length + this.active.length;
95
- }
96
- }
97
- exports.PromiseQueue = PromiseQueue;
@@ -1,25 +0,0 @@
1
- /**
2
- * Copyright 2023 Kapeta Inc.
3
- * SPDX-License-Identifier: BUSL-1.1
4
- */
5
- export type Future<T> = () => Promise<T>;
6
- export type FuturePromise<T> = {
7
- promise: Promise<T>;
8
- resolve: (value: T) => void;
9
- reject: (reason: any) => void;
10
- isResolved: () => boolean;
11
- };
12
- export declare function createFuture<T = void>(): FuturePromise<T>;
13
- export declare class PromiseQueue {
14
- private readonly queue;
15
- private readonly active;
16
- private readonly maxConcurrency;
17
- constructor(maxConcurrency?: number);
18
- private toInternal;
19
- add<T>(future: Future<T>): Promise<T>;
20
- private next;
21
- cancel(): void;
22
- wait(): Promise<void>;
23
- get empty(): boolean;
24
- get length(): number;
25
- }
@@ -1,97 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromiseQueue = exports.createFuture = void 0;
4
- function createFuture() {
5
- let resolved = false;
6
- let resolve = () => {
7
- resolved = true;
8
- };
9
- let reject = () => {
10
- resolved = true;
11
- };
12
- const promise = new Promise((res, rej) => {
13
- resolve = (value) => {
14
- resolved = true;
15
- res(value);
16
- };
17
- reject = (reason) => {
18
- resolved = true;
19
- rej(reason);
20
- };
21
- });
22
- return {
23
- promise,
24
- resolve,
25
- reject,
26
- isResolved: () => resolved,
27
- };
28
- }
29
- exports.createFuture = createFuture;
30
- class PromiseQueue {
31
- queue = [];
32
- active = [];
33
- // Maximum number of concurrent promises
34
- maxConcurrency;
35
- constructor(maxConcurrency = 5) {
36
- this.maxConcurrency = maxConcurrency;
37
- }
38
- toInternal(future) {
39
- let resolve = () => { };
40
- let reject = () => { };
41
- const promise = new Promise((res, rej) => {
42
- resolve = res;
43
- reject = rej;
44
- });
45
- return {
46
- execute: future,
47
- promise,
48
- resolve,
49
- reject,
50
- };
51
- }
52
- add(future) {
53
- const internal = this.toInternal(future);
54
- this.queue.push(internal);
55
- this.next();
56
- return internal.promise;
57
- }
58
- next() {
59
- if (this.active.length >= this.maxConcurrency) {
60
- return false;
61
- }
62
- if (this.queue.length === 0) {
63
- return false;
64
- }
65
- const future = this.queue.shift();
66
- if (!future) {
67
- return false;
68
- }
69
- const promise = future
70
- .execute()
71
- .then(future.resolve)
72
- .catch(future.reject)
73
- .finally(() => {
74
- this.active.splice(this.active.indexOf(promise), 1);
75
- this.next();
76
- });
77
- this.active.push(promise);
78
- return this.next();
79
- }
80
- cancel() {
81
- this.queue.splice(0, this.queue.length);
82
- this.active.splice(0, this.active.length);
83
- }
84
- async wait() {
85
- while (this.queue.length > 0 || this.active.length > 0) {
86
- await Promise.allSettled(this.active);
87
- this.next();
88
- }
89
- }
90
- get empty() {
91
- return this.queue.length === 0 && this.active.length === 0;
92
- }
93
- get length() {
94
- return this.queue.length + this.active.length;
95
- }
96
- }
97
- exports.PromiseQueue = PromiseQueue;