@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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/src/storm/PageGenerator.d.ts +39 -0
- package/dist/cjs/src/storm/PageGenerator.js +143 -0
- package/dist/cjs/src/storm/event-parser.js +11 -0
- package/dist/cjs/src/storm/events.d.ts +34 -1
- package/dist/cjs/src/storm/page-utils.d.ts +10 -0
- package/dist/cjs/src/storm/page-utils.js +65 -6
- package/dist/cjs/src/storm/routes.js +105 -56
- package/dist/cjs/src/storm/stormClient.d.ts +7 -1
- package/dist/cjs/src/storm/stormClient.js +24 -1
- package/dist/cjs/src/storm/stream.d.ts +1 -0
- package/dist/cjs/src/storm/stream.js +1 -0
- package/dist/esm/src/storm/PageGenerator.d.ts +39 -0
- package/dist/esm/src/storm/PageGenerator.js +143 -0
- package/dist/esm/src/storm/event-parser.js +11 -0
- package/dist/esm/src/storm/events.d.ts +34 -1
- package/dist/esm/src/storm/page-utils.d.ts +10 -0
- package/dist/esm/src/storm/page-utils.js +65 -6
- package/dist/esm/src/storm/routes.js +105 -56
- package/dist/esm/src/storm/stormClient.d.ts +7 -1
- package/dist/esm/src/storm/stormClient.js +24 -1
- package/dist/esm/src/storm/stream.d.ts +1 -0
- package/dist/esm/src/storm/stream.js +1 -0
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +181 -0
- package/src/storm/event-parser.ts +12 -0
- package/src/storm/events.ts +43 -1
- package/src/storm/page-utils.ts +88 -12
- package/src/storm/routes.ts +147 -70
- package/src/storm/stormClient.ts +33 -1
- package/src/storm/stream.ts +2 -0
@@ -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
|
-
|
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();
|
package/package.json
CHANGED
@@ -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);
|
package/src/storm/events.ts
CHANGED
@@ -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;
|
package/src/storm/page-utils.ts
CHANGED
@@ -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
|
15
|
-
|
16
|
-
|
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 =
|
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 =
|
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
|
+
}
|