@kapeta/local-cluster-service 0.56.1 → 0.57.0

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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.57.0](https://github.com/kapetacom/local-cluster-service/compare/v0.56.2...v0.57.0) (2024-07-22)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add support for pages and journeys ([743f6fd](https://github.com/kapetacom/local-cluster-service/commit/743f6fdb1851eb2c7a76e79bc68a74f529af9207))
7
+
8
+ ## [0.56.2](https://github.com/kapetacom/local-cluster-service/compare/v0.56.1...v0.56.2) (2024-07-19)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * missing pural on MODEL RETRY ([ca33bdd](https://github.com/kapetacom/local-cluster-service/commit/ca33bdd5e47b0c601d19ddf4149de5e925aa0040))
14
+
1
15
  ## [0.56.1](https://github.com/kapetacom/local-cluster-service/compare/v0.56.0...v0.56.1) (2024-07-17)
2
16
 
3
17
 
@@ -263,11 +263,14 @@ class StormCodegen {
263
263
  }
264
264
  const kapetaYaml = yaml_1.default.stringify(block.content);
265
265
  const blockDefinition = block.content;
266
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
266
267
  // Generate the code for the block using the standard codegen templates
267
268
  const generatedResult = await this.generateBlock(blockDefinition);
268
269
  if (!generatedResult) {
269
270
  return;
270
271
  }
272
+ const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
273
+ await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
271
274
  const allFiles = this.toStormFiles(generatedResult);
272
275
  // Send all the non-ai files to the stream
273
276
  await this.emitStaticFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
@@ -357,7 +360,6 @@ class StormCodegen {
357
360
  if (this.isAborted()) {
358
361
  return;
359
362
  }
360
- const basePath = this.getBasePath(blockDefinition.metadata.name);
361
363
  const screenFilesConverted = screenFiles.map((screenFile) => {
362
364
  return {
363
365
  filename: screenFile.payload.filename,
@@ -397,8 +399,6 @@ class StormCodegen {
397
399
  const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
398
400
  await (0, promises_1.writeFile)(filePath, webRouterFile.content);
399
401
  }
400
- const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
401
- await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
402
402
  const blockRef = block.uri;
403
403
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
404
404
  /* TODO: temporarily disabled - enable again when codegen is more stable
@@ -224,7 +224,7 @@ class StormEventParser {
224
224
  block.apis = [];
225
225
  });
226
226
  break;
227
- case 'MODEL_RETRY':
227
+ case 'MODELS_RETRY':
228
228
  Object.values(this.blocks).forEach((block) => {
229
229
  block.models = [];
230
230
  });
@@ -94,7 +94,7 @@ export interface StormEventCreateDSLResource extends Omit<StormEventCreateDSL, '
94
94
  };
95
95
  }
96
96
  export interface StormEventCreateDSLRetry {
97
- type: 'API_RETRY' | 'MODEL_RETRY';
97
+ type: 'API_RETRY' | 'MODELS_RETRY';
98
98
  reason: string;
99
99
  created: number;
100
100
  payload: {
@@ -266,4 +266,34 @@ export interface StormEventPhases {
266
266
  phaseType: StormEventPhaseType;
267
267
  };
268
268
  }
269
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
269
+ export interface Page {
270
+ name: string;
271
+ description: string;
272
+ content: string;
273
+ path: string;
274
+ method: string;
275
+ }
276
+ export interface StormEventPage {
277
+ type: 'PAGE';
278
+ reason: string;
279
+ created: number;
280
+ payload: Page;
281
+ }
282
+ export interface UserJourneyScreen {
283
+ name: string;
284
+ filename: string;
285
+ requirements: string;
286
+ path: string;
287
+ method: string;
288
+ nextScreens: string[];
289
+ }
290
+ export interface UserJourney {
291
+ screens: UserJourneyScreen[];
292
+ }
293
+ export interface StormEventUserJourney {
294
+ type: 'USER_JOURNEY';
295
+ reason: string;
296
+ created: number;
297
+ payload: UserJourney;
298
+ }
299
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
@@ -21,6 +21,72 @@ const assetManager_1 = require("../assetManager");
21
21
  const router = (0, express_promise_router_1.default)();
22
22
  router.use('/', cors_1.corsHandler);
23
23
  router.use('/', stringBody_1.stringBody);
24
+ router.post('/:handle/ui', async (req, res) => {
25
+ const handle = req.params.handle;
26
+ try {
27
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
28
+ const aiRequest = JSON.parse(req.stringBody ?? '{}');
29
+ const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
30
+ onRequestAborted(req, res, () => {
31
+ userJourneysStream.abort();
32
+ });
33
+ res.set('Content-Type', 'application/x-ndjson');
34
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
35
+ res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
36
+ const promises = {};
37
+ userJourneysStream.on('data', async (data) => {
38
+ try {
39
+ console.log('Processing user journey event', data);
40
+ sendEvent(res, data);
41
+ if (data.type !== 'USER_JOURNEY') {
42
+ return;
43
+ }
44
+ data.payload.screens.forEach((screen) => {
45
+ if (screen.name in promises) {
46
+ return;
47
+ }
48
+ promises[screen.name] = new Promise(async (resolve, reject) => {
49
+ try {
50
+ const screenStream = await stormClient_1.stormClient.createUIPage({
51
+ prompt: screen.requirements,
52
+ method: screen.method,
53
+ path: screen.path,
54
+ description: screen.requirements,
55
+ name: screen.name,
56
+ filename: screen.filename,
57
+ }, conversationId);
58
+ screenStream.on('data', (screenData) => {
59
+ console.log('Processing screen event', screenData);
60
+ sendEvent(res, screenData);
61
+ });
62
+ screenStream.on('end', () => {
63
+ resolve();
64
+ });
65
+ }
66
+ catch (e) {
67
+ reject(e);
68
+ }
69
+ });
70
+ });
71
+ }
72
+ catch (e) {
73
+ console.error('Failed to process event', e);
74
+ }
75
+ });
76
+ await waitForStormStream(userJourneysStream);
77
+ if (userJourneysStream.isAborted()) {
78
+ return;
79
+ }
80
+ await Promise.all(Object.values(promises));
81
+ sendDone(res);
82
+ }
83
+ catch (err) {
84
+ sendError(err, res);
85
+ if (!res.closed) {
86
+ res.end();
87
+ }
88
+ }
89
+ });
24
90
  router.post('/:handle/all', async (req, res) => {
25
91
  const handle = req.params.handle;
26
92
  try {
@@ -1,12 +1,23 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
+ interface UIPagePrompt {
5
+ name: string;
6
+ filename: string;
7
+ prompt: string;
8
+ path: string;
9
+ method: string;
10
+ description: string;
11
+ }
4
12
  declare class StormClient {
5
13
  private readonly _baseUrl;
6
14
  constructor();
7
15
  private createOptions;
8
16
  private send;
9
17
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
18
+ createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
19
+ createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
20
+ createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
10
21
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
11
22
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
23
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
@@ -28,7 +28,7 @@ class StormClient {
28
28
  const api = new nodejs_api_client_1.KapetaAPI();
29
29
  if (api.hasToken()) {
30
30
  const token = await api.getAccessToken();
31
- headers['Authorization'] = `Bearer ${token}`;
31
+ //headers['Authorization'] = `Bearer ${token}`;
32
32
  }
33
33
  if (body.conversationId) {
34
34
  headers[exports.ConversationIdHeader] = body.conversationId;
@@ -80,6 +80,24 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ createUIPages(prompt, conversationId) {
84
+ return this.send('/v2/ui/pages', {
85
+ prompt: prompt,
86
+ conversationId,
87
+ });
88
+ }
89
+ createUIUserJourneys(prompt, conversationId) {
90
+ return this.send('/v2/ui/user-journeys', {
91
+ prompt: prompt,
92
+ conversationId,
93
+ });
94
+ }
95
+ createUIPage(prompt, conversationId) {
96
+ return this.send('/v2/ui/page', {
97
+ prompt: prompt,
98
+ conversationId,
99
+ });
100
+ }
83
101
  listScreens(prompt, conversationId) {
84
102
  return this.send('/v2/ui/list', {
85
103
  prompt,
@@ -263,11 +263,14 @@ class StormCodegen {
263
263
  }
264
264
  const kapetaYaml = yaml_1.default.stringify(block.content);
265
265
  const blockDefinition = block.content;
266
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
266
267
  // Generate the code for the block using the standard codegen templates
267
268
  const generatedResult = await this.generateBlock(blockDefinition);
268
269
  if (!generatedResult) {
269
270
  return;
270
271
  }
272
+ const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
273
+ await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
271
274
  const allFiles = this.toStormFiles(generatedResult);
272
275
  // Send all the non-ai files to the stream
273
276
  await this.emitStaticFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
@@ -357,7 +360,6 @@ class StormCodegen {
357
360
  if (this.isAborted()) {
358
361
  return;
359
362
  }
360
- const basePath = this.getBasePath(blockDefinition.metadata.name);
361
363
  const screenFilesConverted = screenFiles.map((screenFile) => {
362
364
  return {
363
365
  filename: screenFile.payload.filename,
@@ -397,8 +399,6 @@ class StormCodegen {
397
399
  const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
398
400
  await (0, promises_1.writeFile)(filePath, webRouterFile.content);
399
401
  }
400
- const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
401
- await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
402
402
  const blockRef = block.uri;
403
403
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
404
404
  /* TODO: temporarily disabled - enable again when codegen is more stable
@@ -224,7 +224,7 @@ class StormEventParser {
224
224
  block.apis = [];
225
225
  });
226
226
  break;
227
- case 'MODEL_RETRY':
227
+ case 'MODELS_RETRY':
228
228
  Object.values(this.blocks).forEach((block) => {
229
229
  block.models = [];
230
230
  });
@@ -94,7 +94,7 @@ export interface StormEventCreateDSLResource extends Omit<StormEventCreateDSL, '
94
94
  };
95
95
  }
96
96
  export interface StormEventCreateDSLRetry {
97
- type: 'API_RETRY' | 'MODEL_RETRY';
97
+ type: 'API_RETRY' | 'MODELS_RETRY';
98
98
  reason: string;
99
99
  created: number;
100
100
  payload: {
@@ -266,4 +266,34 @@ export interface StormEventPhases {
266
266
  phaseType: StormEventPhaseType;
267
267
  };
268
268
  }
269
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
269
+ export interface Page {
270
+ name: string;
271
+ description: string;
272
+ content: string;
273
+ path: string;
274
+ method: string;
275
+ }
276
+ export interface StormEventPage {
277
+ type: 'PAGE';
278
+ reason: string;
279
+ created: number;
280
+ payload: Page;
281
+ }
282
+ export interface UserJourneyScreen {
283
+ name: string;
284
+ filename: string;
285
+ requirements: string;
286
+ path: string;
287
+ method: string;
288
+ nextScreens: string[];
289
+ }
290
+ export interface UserJourney {
291
+ screens: UserJourneyScreen[];
292
+ }
293
+ export interface StormEventUserJourney {
294
+ type: 'USER_JOURNEY';
295
+ reason: string;
296
+ created: number;
297
+ payload: UserJourney;
298
+ }
299
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
@@ -21,6 +21,72 @@ const assetManager_1 = require("../assetManager");
21
21
  const router = (0, express_promise_router_1.default)();
22
22
  router.use('/', cors_1.corsHandler);
23
23
  router.use('/', stringBody_1.stringBody);
24
+ router.post('/:handle/ui', async (req, res) => {
25
+ const handle = req.params.handle;
26
+ try {
27
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
28
+ const aiRequest = JSON.parse(req.stringBody ?? '{}');
29
+ const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
30
+ onRequestAborted(req, res, () => {
31
+ userJourneysStream.abort();
32
+ });
33
+ res.set('Content-Type', 'application/x-ndjson');
34
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
35
+ res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
36
+ const promises = {};
37
+ userJourneysStream.on('data', async (data) => {
38
+ try {
39
+ console.log('Processing user journey event', data);
40
+ sendEvent(res, data);
41
+ if (data.type !== 'USER_JOURNEY') {
42
+ return;
43
+ }
44
+ data.payload.screens.forEach((screen) => {
45
+ if (screen.name in promises) {
46
+ return;
47
+ }
48
+ promises[screen.name] = new Promise(async (resolve, reject) => {
49
+ try {
50
+ const screenStream = await stormClient_1.stormClient.createUIPage({
51
+ prompt: screen.requirements,
52
+ method: screen.method,
53
+ path: screen.path,
54
+ description: screen.requirements,
55
+ name: screen.name,
56
+ filename: screen.filename,
57
+ }, conversationId);
58
+ screenStream.on('data', (screenData) => {
59
+ console.log('Processing screen event', screenData);
60
+ sendEvent(res, screenData);
61
+ });
62
+ screenStream.on('end', () => {
63
+ resolve();
64
+ });
65
+ }
66
+ catch (e) {
67
+ reject(e);
68
+ }
69
+ });
70
+ });
71
+ }
72
+ catch (e) {
73
+ console.error('Failed to process event', e);
74
+ }
75
+ });
76
+ await waitForStormStream(userJourneysStream);
77
+ if (userJourneysStream.isAborted()) {
78
+ return;
79
+ }
80
+ await Promise.all(Object.values(promises));
81
+ sendDone(res);
82
+ }
83
+ catch (err) {
84
+ sendError(err, res);
85
+ if (!res.closed) {
86
+ res.end();
87
+ }
88
+ }
89
+ });
24
90
  router.post('/:handle/all', async (req, res) => {
25
91
  const handle = req.params.handle;
26
92
  try {
@@ -1,12 +1,23 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
+ interface UIPagePrompt {
5
+ name: string;
6
+ filename: string;
7
+ prompt: string;
8
+ path: string;
9
+ method: string;
10
+ description: string;
11
+ }
4
12
  declare class StormClient {
5
13
  private readonly _baseUrl;
6
14
  constructor();
7
15
  private createOptions;
8
16
  private send;
9
17
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
18
+ createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
19
+ createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
20
+ createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
10
21
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
11
22
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
23
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
@@ -28,7 +28,7 @@ class StormClient {
28
28
  const api = new nodejs_api_client_1.KapetaAPI();
29
29
  if (api.hasToken()) {
30
30
  const token = await api.getAccessToken();
31
- headers['Authorization'] = `Bearer ${token}`;
31
+ //headers['Authorization'] = `Bearer ${token}`;
32
32
  }
33
33
  if (body.conversationId) {
34
34
  headers[exports.ConversationIdHeader] = body.conversationId;
@@ -80,6 +80,24 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ createUIPages(prompt, conversationId) {
84
+ return this.send('/v2/ui/pages', {
85
+ prompt: prompt,
86
+ conversationId,
87
+ });
88
+ }
89
+ createUIUserJourneys(prompt, conversationId) {
90
+ return this.send('/v2/ui/user-journeys', {
91
+ prompt: prompt,
92
+ conversationId,
93
+ });
94
+ }
95
+ createUIPage(prompt, conversationId) {
96
+ return this.send('/v2/ui/page', {
97
+ prompt: prompt,
98
+ conversationId,
99
+ });
100
+ }
83
101
  listScreens(prompt, conversationId) {
84
102
  return this.send('/v2/ui/list', {
85
103
  prompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.56.1",
3
+ "version": "0.57.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -299,6 +299,7 @@ export class StormCodegen {
299
299
 
300
300
  const kapetaYaml = YAML.stringify(block.content);
301
301
  const blockDefinition = block.content;
302
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
302
303
 
303
304
  // Generate the code for the block using the standard codegen templates
304
305
  const generatedResult = await this.generateBlock(blockDefinition);
@@ -306,6 +307,9 @@ export class StormCodegen {
306
307
  return;
307
308
  }
308
309
 
310
+ const kapetaYmlPath = join(basePath, 'kapeta.yml');
311
+ await writeFile(kapetaYmlPath, kapetaYaml);
312
+
309
313
  const allFiles = this.toStormFiles(generatedResult);
310
314
 
311
315
  // Send all the non-ai files to the stream
@@ -421,7 +425,6 @@ export class StormCodegen {
421
425
  if (this.isAborted()) {
422
426
  return;
423
427
  }
424
- const basePath = this.getBasePath(blockDefinition.metadata.name);
425
428
 
426
429
  const screenFilesConverted = screenFiles.map((screenFile) => {
427
430
  return {
@@ -485,8 +488,6 @@ export class StormCodegen {
485
488
  await writeFile(filePath, webRouterFile.content);
486
489
  }
487
490
 
488
- const kapetaYmlPath = join(basePath, 'kapeta.yml');
489
- await writeFile(kapetaYmlPath, kapetaYaml);
490
491
 
491
492
  const blockRef = block.uri;
492
493
 
@@ -340,7 +340,7 @@ export class StormEventParser {
340
340
  block.apis = [];
341
341
  });
342
342
  break;
343
- case 'MODEL_RETRY':
343
+ case 'MODELS_RETRY':
344
344
  Object.values(this.blocks).forEach((block) => {
345
345
  block.models = [];
346
346
  });
@@ -120,7 +120,7 @@ export interface StormEventCreateDSLResource extends Omit<StormEventCreateDSL, '
120
120
  }
121
121
 
122
122
  export interface StormEventCreateDSLRetry {
123
- type: 'API_RETRY' | 'MODEL_RETRY';
123
+ type: 'API_RETRY' | 'MODELS_RETRY';
124
124
  reason: string;
125
125
  created: number;
126
126
  payload: {
@@ -316,6 +316,43 @@ export interface StormEventPhases {
316
316
  };
317
317
  }
318
318
 
319
+ export interface Page {
320
+ name: string;
321
+ description: string;
322
+ content: string;
323
+ path: string;
324
+ method: string;
325
+ }
326
+
327
+ // Event for creating a page
328
+ export interface StormEventPage {
329
+ type: 'PAGE';
330
+ reason: string;
331
+ created: number;
332
+ payload: Page;
333
+ }
334
+
335
+ export interface UserJourneyScreen {
336
+ name: string;
337
+ filename: string;
338
+ requirements: string;
339
+ path: string;
340
+ method: string;
341
+ nextScreens: string[];
342
+ }
343
+
344
+ export interface UserJourney {
345
+ screens: UserJourneyScreen[];
346
+ }
347
+
348
+ // Event for defining a user journey
349
+ export interface StormEventUserJourney {
350
+ type: 'USER_JOURNEY';
351
+ reason: string;
352
+ created: number;
353
+ payload: UserJourney;
354
+ }
355
+
319
356
  export type StormEvent =
320
357
  | StormEventCreateBlock
321
358
  | StormEventCreateConnection
@@ -340,4 +377,6 @@ export type StormEvent =
340
377
  | StormEventBlockReady
341
378
  | StormEventPhases
342
379
  | StormEventBlockStatus
343
- | StormEventCreateDSLRetry;
380
+ | StormEventCreateDSLRetry
381
+ | StormEventUserJourney
382
+ | StormEventPage;
@@ -29,6 +29,84 @@ const router = Router();
29
29
  router.use('/', corsHandler);
30
30
  router.use('/', stringBody);
31
31
 
32
+ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
33
+ const handle = req.params.handle as string;
34
+ try {
35
+ const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
36
+
37
+ const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
38
+
39
+ const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
40
+
41
+ onRequestAborted(req, res, () => {
42
+ userJourneysStream.abort();
43
+ });
44
+
45
+ res.set('Content-Type', 'application/x-ndjson');
46
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
47
+ res.set(ConversationIdHeader, userJourneysStream.getConversationId());
48
+
49
+ const promises: { [key: string]: Promise<void> } = {};
50
+
51
+ userJourneysStream.on('data', async (data: StormEvent) => {
52
+ try {
53
+ console.log('Processing user journey event', data);
54
+ sendEvent(res, data);
55
+ if (data.type !== 'USER_JOURNEY') {
56
+ return;
57
+ }
58
+
59
+ data.payload.screens.forEach((screen) => {
60
+ if (screen.name in promises) {
61
+ return;
62
+ }
63
+ promises[screen.name] = new Promise(async (resolve, reject) => {
64
+ try {
65
+ const screenStream = await stormClient.createUIPage(
66
+ {
67
+ prompt: screen.requirements,
68
+ method: screen.method,
69
+ path: screen.path,
70
+ description: screen.requirements,
71
+ name: screen.name,
72
+ filename: screen.filename,
73
+ },
74
+ conversationId
75
+ );
76
+ screenStream.on('data', (screenData: StormEvent) => {
77
+ console.log('Processing screen event', screenData);
78
+ sendEvent(res, screenData);
79
+ });
80
+ screenStream.on('end', () => {
81
+ resolve();
82
+ });
83
+ } catch (e: any) {
84
+ reject(e);
85
+ }
86
+ });
87
+ });
88
+ } catch (e) {
89
+ console.error('Failed to process event', e);
90
+ }
91
+ });
92
+
93
+ await waitForStormStream(userJourneysStream);
94
+
95
+ if (userJourneysStream.isAborted()) {
96
+ return;
97
+ }
98
+
99
+ await Promise.all(Object.values(promises));
100
+
101
+ sendDone(res);
102
+ } catch (err: any) {
103
+ sendError(err, res);
104
+ if (!res.closed) {
105
+ res.end();
106
+ }
107
+ }
108
+ });
109
+
32
110
  router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
33
111
  const handle = req.params.handle as string;
34
112
 
@@ -20,6 +20,15 @@ export const STORM_ID = 'storm';
20
20
 
21
21
  export const ConversationIdHeader = 'Conversation-Id';
22
22
 
23
+ interface UIPagePrompt {
24
+ name: string;
25
+ filename: string;
26
+ prompt: string;
27
+ path: string;
28
+ method: string;
29
+ description: string;
30
+ }
31
+
23
32
  class StormClient {
24
33
  private readonly _baseUrl: string;
25
34
 
@@ -39,7 +48,7 @@ class StormClient {
39
48
  const api = new KapetaAPI();
40
49
  if (api.hasToken()) {
41
50
  const token = await api.getAccessToken();
42
- headers['Authorization'] = `Bearer ${token}`;
51
+ //headers['Authorization'] = `Bearer ${token}`;
43
52
  }
44
53
 
45
54
  if (body.conversationId) {
@@ -113,6 +122,27 @@ class StormClient {
113
122
  });
114
123
  }
115
124
 
125
+ public createUIPages(prompt: string, conversationId?: string) {
126
+ return this.send('/v2/ui/pages', {
127
+ prompt: prompt,
128
+ conversationId,
129
+ });
130
+ }
131
+
132
+ public createUIUserJourneys(prompt: string, conversationId?: string) {
133
+ return this.send('/v2/ui/user-journeys', {
134
+ prompt: prompt,
135
+ conversationId,
136
+ });
137
+ }
138
+
139
+ public createUIPage(prompt: UIPagePrompt, conversationId?: string) {
140
+ return this.send('/v2/ui/page', {
141
+ prompt: prompt,
142
+ conversationId,
143
+ });
144
+ }
145
+
116
146
  public listScreens(prompt: StormUIListPrompt, conversationId?: string) {
117
147
  return this.send('/v2/ui/list', {
118
148
  prompt,