@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.
@@ -23,6 +23,9 @@ export interface UIPagePrompt {
23
23
  storage_prefix: string;
24
24
  shell_page?: string;
25
25
  }
26
+ export interface UIPageSamplePrompt extends UIPagePrompt {
27
+ variantId: string;
28
+ }
26
29
  export interface UIPageEditPrompt {
27
30
  planDescription: string;
28
31
  blockDescription: string;
@@ -44,7 +47,9 @@ declare class StormClient {
44
47
  createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
45
48
  createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
46
49
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
47
- createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
50
+ createUILandingPages(prompt: string, conversationId?: string): Promise<StormStream>;
51
+ createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
52
+ classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
48
53
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
49
54
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
50
55
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
@@ -53,6 +58,7 @@ declare class StormClient {
53
58
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
54
59
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
55
60
  generateCode(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
61
+ deleteUIPageConversation(conversationId: string): Promise<string>;
56
62
  }
57
63
  export declare const stormClient: StormClient;
58
64
  export {};
@@ -95,13 +95,25 @@ class StormClient {
95
95
  createUIShells(prompt, conversationId) {
96
96
  return this.send('/v2/ui/shells', {
97
97
  prompt: JSON.stringify(prompt),
98
+ });
99
+ }
100
+ createUILandingPages(prompt, conversationId) {
101
+ return this.send('/v2/ui/landing-pages', {
102
+ prompt: prompt,
98
103
  conversationId,
99
104
  });
100
105
  }
101
- createUIPage(prompt, conversationId) {
106
+ createUIPage(prompt, conversationId, history) {
102
107
  return this.send('/v2/ui/page', {
103
108
  prompt: prompt,
104
109
  conversationId,
110
+ history,
111
+ });
112
+ }
113
+ classifyUIReferences(prompt, conversationId) {
114
+ return this.send('/v2/ui/references', {
115
+ prompt: prompt,
116
+ conversationId,
105
117
  });
106
118
  }
107
119
  editPages(prompt, conversationId) {
@@ -152,5 +164,16 @@ class StormClient {
152
164
  conversationId: conversationId,
153
165
  });
154
166
  }
167
+ async deleteUIPageConversation(conversationId) {
168
+ const options = await this.createOptions('/v2/ui/page', 'DELETE', {
169
+ prompt: '',
170
+ conversationId: conversationId,
171
+ });
172
+ const response = await fetch(options.url, {
173
+ method: options.method,
174
+ headers: options.headers,
175
+ });
176
+ return response.text();
177
+ }
155
178
  }
156
179
  exports.stormClient = new StormClient();
@@ -35,6 +35,7 @@ export interface ConversationItem {
35
35
  }
36
36
  export interface StormContextRequest<T = string> {
37
37
  conversationId?: string;
38
+ history?: ConversationItem[];
38
39
  prompt: T;
39
40
  }
40
41
  export interface StormCreateBlockRequest {
@@ -32,6 +32,7 @@ class StormStream extends node_events_1.EventEmitter {
32
32
  }
33
33
  catch (e) {
34
34
  this.emit('error', e);
35
+ console.warn('Failed to parse JSON line', e, line);
35
36
  }
36
37
  }
37
38
  end() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.61.2",
3
+ "version": "0.62.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ import uuid from 'node-uuid';
7
+ import { stormClient, UIPagePrompt } from './stormClient';
8
+ import { ReferenceClassification, StormEvent, StormEventPage, StormEventReferenceClassification } from './events';
9
+ import { EventEmitter } from 'node:events';
10
+ import { PromiseQueue } from './PromiseQueue';
11
+
12
+ export class PageQueue extends EventEmitter {
13
+ private readonly queue: PromiseQueue;
14
+ private readonly systemId: string;
15
+ private readonly references: Map<string, PageGenerator> = new Map();
16
+
17
+ constructor(systemId: string, concurrency: number = 5) {
18
+ super();
19
+ this.systemId = systemId;
20
+ this.queue = new PromiseQueue(concurrency);
21
+ }
22
+
23
+ on(event: 'event', listener: (data: StormEvent) => void): this;
24
+ on(event: 'page', listener: (data: StormEventPage) => void): this;
25
+
26
+ on(event: string, listener: (...args: any[]) => void): this {
27
+ return super.on(event, listener);
28
+ }
29
+
30
+ emit(type: 'event', event: StormEvent): boolean;
31
+ emit(type: 'page', event: StormEventPage): boolean;
32
+ emit(eventName: string | symbol, ...args: any[]): boolean {
33
+ return super.emit(eventName, ...args);
34
+ }
35
+
36
+ public addPrompt(initialPrompt: UIPagePrompt) {
37
+ if (this.references.has(initialPrompt.path)) {
38
+ console.log('Ignoring duplicate prompt', initialPrompt.path);
39
+ return Promise.resolve();
40
+ }
41
+ console.log('processing prompt', initialPrompt.path);
42
+ const generator = new PageGenerator(initialPrompt);
43
+ this.references.set(initialPrompt.path, generator);
44
+ return this.addPageGenerator(generator);
45
+ }
46
+
47
+ private async addPageGenerator(generator: PageGenerator) {
48
+ generator.on('event', (event: StormEvent) => this.emit('event', event));
49
+ generator.on('page_refs', ({ event, references }) => {
50
+ this.emit('page', event);
51
+ references.forEach((reference) => {
52
+ if (
53
+ reference.url.startsWith('#') ||
54
+ reference.url.startsWith('javascript:') ||
55
+ reference.url.startsWith('http://') ||
56
+ reference.url.startsWith('https://')
57
+ ) {
58
+ return;
59
+ }
60
+
61
+ switch (reference.type) {
62
+ case 'image':
63
+ console.log('Ignoring image reference', reference);
64
+ break;
65
+ case 'css':
66
+ case 'javascript':
67
+ //console.log('Ignoring reference', reference);
68
+ break;
69
+ case 'html':
70
+ console.log('Adding page generator for', reference);
71
+ const paths = Array.from(this.references.keys());
72
+ this.addPrompt({
73
+ name: reference.name,
74
+ title: reference.title,
75
+ path: reference.url,
76
+ method: 'GET',
77
+ storage_prefix: this.systemId + '_',
78
+ prompt:
79
+ `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
80
+ `The page was referenced from this page: \`\`\`html\n${event.payload.content}\n\`\`\`\n` +
81
+ (paths.length > 0
82
+ ? `\nThese paths are already implemented:\n- ${paths.join('\n - ')}\n\n`
83
+ : ''),
84
+ description: reference.description,
85
+ filename: '',
86
+ });
87
+ break;
88
+ }
89
+ });
90
+ });
91
+ return this.queue.add(() => generator.generate());
92
+ }
93
+
94
+ public cancel() {
95
+ this.queue.cancel();
96
+ }
97
+
98
+ public wait() {
99
+ return this.queue.wait();
100
+ }
101
+ }
102
+
103
+ export class PageGenerator extends EventEmitter {
104
+ private readonly conversationId: string;
105
+ private prompt: UIPagePrompt;
106
+
107
+ constructor(prompt: UIPagePrompt, conversationId: string = uuid.v4()) {
108
+ super();
109
+ this.conversationId = conversationId;
110
+ this.prompt = prompt;
111
+ }
112
+
113
+ on(event: 'event', listener: (data: StormEvent) => void): this;
114
+ on(
115
+ event: 'page_refs',
116
+ listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => void
117
+ ): this;
118
+
119
+ on(event: string, listener: (...args: any[]) => void): this {
120
+ return super.on(event, listener);
121
+ }
122
+
123
+ emit(type: 'event', event: StormEvent): boolean;
124
+ emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
125
+ emit(eventName: string | symbol, ...args: any[]): boolean {
126
+ return super.emit(eventName, ...args);
127
+ }
128
+
129
+ public async generate() {
130
+ return new Promise<void>(async (resolve) => {
131
+ const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
132
+
133
+ const promises: Promise<void>[] = [];
134
+
135
+ screenStream.on('data', (event: StormEvent) => {
136
+ if (event.type === 'PAGE') {
137
+ event.payload.conversationId = this.conversationId;
138
+
139
+ promises.push(
140
+ (async () => {
141
+ const references = await this.resolveReferences(event.payload.content);
142
+ //console.log('Resolved references for page', references, event.payload);
143
+ this.emit('page_refs', {
144
+ event,
145
+ references,
146
+ });
147
+ })()
148
+ );
149
+ return;
150
+ }
151
+
152
+ this.emit('event', event);
153
+ });
154
+
155
+ screenStream.on('end', () => {
156
+ Promise.allSettled(promises).finally(resolve);
157
+ });
158
+
159
+ await screenStream.waitForDone();
160
+ });
161
+ }
162
+
163
+ private async resolveReferences(content: string) {
164
+ const referenceStream = await stormClient.classifyUIReferences(content);
165
+
166
+ const references: ReferenceClassification[] = [];
167
+
168
+ referenceStream.on('data', (referenceData: StormEvent) => {
169
+ if (referenceData.type !== 'REF_CLASSIFICATION') {
170
+ return;
171
+ }
172
+
173
+ //console.log('Processing reference classification', referenceData);
174
+ references.push(referenceData.payload);
175
+ });
176
+
177
+ await referenceStream.waitForDone();
178
+
179
+ return references;
180
+ }
181
+ }
@@ -357,6 +357,18 @@ export class StormEventParser {
357
357
  this.blocks[evt.payload.blockName].models = [];
358
358
  }
359
359
  break;
360
+
361
+ case 'API_STREAM_CHUNK':
362
+ case 'API_STREAM_CHUNK_RESET':
363
+ case 'API_STREAM_DONE':
364
+ case 'API_STREAM_FAILED':
365
+ case 'API_STREAM_STATE':
366
+ case 'API_STREAM_START':
367
+ if ('blockName' in evt.payload) {
368
+ evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString();
369
+ evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef);
370
+ }
371
+ break;
360
372
  }
361
373
 
362
374
  return await this.toResult(handle);
@@ -263,6 +263,11 @@ export interface StormEventFileChunk extends StormEventFileBase {
263
263
  };
264
264
  }
265
265
 
266
+ export interface StormEventApiBase {
267
+ type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
268
+ payload: StormEventFileBasePayload;
269
+ }
270
+
266
271
  export interface StormEventBlockReady {
267
272
  type: 'BLOCK_READY';
268
273
  reason: string;
@@ -406,6 +411,40 @@ export interface StormEventPromptImprove {
406
411
  };
407
412
  }
408
413
 
414
+ export interface LandingPage {
415
+ name: string;
416
+ title: string;
417
+ filename: string;
418
+ create_prompt: string;
419
+ path: string;
420
+ archetype: string;
421
+ requires_authentication: boolean;
422
+ }
423
+
424
+ // Event for defining a landing pages
425
+ export interface StormEventLandingPage {
426
+ type: 'LANDING_PAGE';
427
+ reason: string;
428
+ created: number;
429
+ payload: LandingPage;
430
+ }
431
+
432
+ export interface ReferenceClassification {
433
+ name: string;
434
+ title: string;
435
+ url: string;
436
+ description: string;
437
+ type: 'image' | 'css' | 'javascript' | 'html';
438
+ source: 'local' | 'cdn' | 'example';
439
+ }
440
+ // Event for reference classification
441
+ export interface StormEventReferenceClassification {
442
+ type: 'REF_CLASSIFICATION';
443
+ reason: string;
444
+ created: number;
445
+ payload: ReferenceClassification;
446
+ }
447
+
409
448
  export type StormEvent =
410
449
  | StormEventCreateBlock
411
450
  | StormEventCreateConnection
@@ -435,4 +474,7 @@ export type StormEvent =
435
474
  | StormEventUIShell
436
475
  | StormEventPage
437
476
  | StormEventPageUrl
438
- | StormEventPromptImprove;
477
+ | StormEventPromptImprove
478
+ | StormEventLandingPage
479
+ | StormEventReferenceClassification
480
+ | StormEventApiBase;
@@ -7,18 +7,23 @@ import { Response } from 'express';
7
7
  import os from 'node:os';
8
8
  import Path from 'path';
9
9
  import FS from 'fs-extra';
10
+ import FSExtra from 'fs-extra';
11
+ import { ConversationItem } from './stream';
12
+ import exp from 'node:constants';
10
13
 
11
14
  export const SystemIdHeader = 'System-Id';
12
15
 
16
+ function normalizePath(path: string) {
17
+ return path
18
+ .replace(/\?.*$/gi, '')
19
+ .replace(/:[a-z][a-z_]*\b/gi, '*')
20
+ .replace(/\{[a-z]+}/gi, '*');
21
+ }
22
+
13
23
  export async function writePageToDisk(systemId: string, event: StormEventPage) {
14
- const path = Path.join(
15
- os.tmpdir(),
16
- 'ai-systems',
17
- systemId,
18
- event.payload.path,
19
- event.payload.method.toLowerCase(),
20
- 'index.html'
21
- );
24
+ const baseDir = getBaseDir(systemId);
25
+ const filePath = getFilePath(event.payload.method);
26
+ const path = Path.join(baseDir, normalizePath(event.payload.path), filePath);
22
27
  await FS.ensureDir(Path.dirname(path));
23
28
  await FS.writeFile(path, event.payload.content);
24
29
 
@@ -29,9 +34,58 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
29
34
  };
30
35
  }
31
36
 
37
+ function getBaseDir(systemId: string) {
38
+ return Path.join(os.tmpdir(), 'ai-systems', systemId);
39
+ }
40
+
41
+ function getFilePath(method: string) {
42
+ return Path.join(method.toLowerCase(), 'index.html');
43
+ }
44
+
45
+ export function resolveReadPath(systemId: string, path: string, method: string) {
46
+ const baseDir = getBaseDir(systemId);
47
+
48
+ path = normalizePath(path);
49
+
50
+ const filePath = getFilePath(method);
51
+
52
+ const fullPath = Path.join(baseDir, path, filePath);
53
+
54
+ if (FS.existsSync(fullPath)) {
55
+ return fullPath;
56
+ }
57
+
58
+ const parts = path.split(/\*/g);
59
+
60
+ let currentPath = '';
61
+
62
+ for (let part in parts) {
63
+ const thisPath = Path.join(currentPath, part);
64
+ const starPath = Path.join(currentPath, '*');
65
+ const thisPathDir = Path.join(baseDir, thisPath);
66
+ const starPathDir = Path.join(baseDir, starPath);
67
+
68
+ if (FS.existsSync(thisPathDir)) {
69
+ currentPath = thisPath;
70
+ continue;
71
+ }
72
+
73
+ if (FS.existsSync(starPathDir)) {
74
+ currentPath = starPath;
75
+ continue;
76
+ }
77
+
78
+ console.log('Path not found', thisPathDir, starPathDir);
79
+ // Path not found
80
+ return null;
81
+ }
82
+
83
+ return Path.join(baseDir, currentPath, filePath);
84
+ }
85
+
32
86
  export function readPageFromDiskAsString(systemId: string, path: string, method: string) {
33
- const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
34
- if (!FS.existsSync(filePath)) {
87
+ const filePath = resolveReadPath(systemId, path, method);
88
+ if (!filePath || !FS.existsSync(filePath)) {
35
89
  return null;
36
90
  }
37
91
 
@@ -39,8 +93,8 @@ export function readPageFromDiskAsString(systemId: string, path: string, method:
39
93
  }
40
94
 
41
95
  export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
42
- const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
43
- if (!FS.existsSync(filePath)) {
96
+ const filePath = resolveReadPath(systemId, path, method);
97
+ if (!filePath || !FS.existsSync(filePath)) {
44
98
  res.status(404).send('Page not found');
45
99
  return;
46
100
  }
@@ -51,3 +105,25 @@ export function readPageFromDisk(systemId: string, path: string, method: string,
51
105
  res.write(content);
52
106
  res.end();
53
107
  }
108
+
109
+ export interface Conversation {
110
+ messages: ConversationItem[];
111
+ variantId: string;
112
+ type: 'page';
113
+ filename: string;
114
+ }
115
+
116
+ export function readConversationFromFile(filename: string): Conversation[] {
117
+ if (!FS.existsSync(filename)) {
118
+ return [];
119
+ }
120
+ const content = FS.readFileSync(filename).toString();
121
+ if (!content.trim()) {
122
+ return [];
123
+ }
124
+ return content.split(/\n/g).map((line) => JSON.parse(line) as Conversation);
125
+ }
126
+
127
+ export function writeConversationToFile(filename: string, conversations: Conversation[]) {
128
+ FS.writeFileSync(filename, conversations.map((conversation) => JSON.stringify(conversation)).join('\n'));
129
+ }