@kapeta/local-cluster-service 0.70.12 → 0.71.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 CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.71.1](https://github.com/kapetacom/local-cluster-service/compare/v0.71.0...v0.71.1) (2024-09-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * emit all events except chunks from screen endpoints ([8e67b58](https://github.com/kapetacom/local-cluster-service/commit/8e67b58b6cc70c359556edfc61dd904254f9d3bc))
7
+ * replace fetch retry handler library to avoid fetch failed ([59a4342](https://github.com/kapetacom/local-cluster-service/commit/59a4342203cd8efb706beba254284ce321fe313a))
8
+
9
+ # [0.71.0](https://github.com/kapetacom/local-cluster-service/compare/v0.70.12...v0.71.0) (2024-09-16)
10
+
11
+
12
+ ### Features
13
+
14
+ * Add two new phases ([d2805a8](https://github.com/kapetacom/local-cluster-service/commit/d2805a89d7e21b6f30674ae852ddc356954e7d79))
15
+
1
16
  ## [0.70.12](https://github.com/kapetacom/local-cluster-service/compare/v0.70.11...v0.70.12) (2024-09-13)
2
17
 
3
18
 
@@ -271,6 +271,8 @@ export interface StormEventDefinitionChange {
271
271
  payload: StormDefinitions;
272
272
  }
273
273
  export declare enum StormEventPhaseType {
274
+ IMPLEMENT_APIS = "IMPLEMENT_APIS",// Implement APIs in the html pages
275
+ COMPOSE_SYSTEM_PROMPT = "COMPOSE_SYSTEM_PROMPT",// Compose system prompt for bottom-up approach
274
276
  META = "META",
275
277
  DEFINITIONS = "DEFINITIONS",
276
278
  IMPLEMENTATION = "IMPLEMENTATION",
@@ -11,6 +11,8 @@ var StormEventBlockStatusType;
11
11
  })(StormEventBlockStatusType || (exports.StormEventBlockStatusType = StormEventBlockStatusType = {}));
12
12
  var StormEventPhaseType;
13
13
  (function (StormEventPhaseType) {
14
+ StormEventPhaseType["IMPLEMENT_APIS"] = "IMPLEMENT_APIS";
15
+ StormEventPhaseType["COMPOSE_SYSTEM_PROMPT"] = "COMPOSE_SYSTEM_PROMPT";
14
16
  StormEventPhaseType["META"] = "META";
15
17
  StormEventPhaseType["DEFINITIONS"] = "DEFINITIONS";
16
18
  StormEventPhaseType["IMPLEMENTATION"] = "IMPLEMENTATION";
@@ -72,6 +72,10 @@ router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
72
72
  const systemId = req.params.systemId;
73
73
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
74
74
  const destDir = (0, page_utils_1.getSystemBaseImplDir)(systemId);
75
+ res.set('Content-Type', 'application/x-ndjson');
76
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
77
+ res.set(stormClient_1.ConversationIdHeader, systemId);
78
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.IMPLEMENT_APIS));
75
79
  await (0, utils_1.copyDirectory)(srcDir, destDir, async (fileName, content) => {
76
80
  const result = await stormClient_1.stormClient.implementAPIClients({
77
81
  content: content,
@@ -79,8 +83,11 @@ router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
79
83
  });
80
84
  return result;
81
85
  });
86
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.IMPLEMENT_APIS));
87
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
82
88
  const pages = (0, utils_1.readPages)(destDir);
83
89
  const prompt = await stormClient_1.stormClient.generatePrompt(pages);
90
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
84
91
  req.query.systemId = systemId;
85
92
  const promptRequest = {
86
93
  prompt: prompt,
@@ -122,6 +129,12 @@ router.post('/ui/screen', async (req, res) => {
122
129
  console.error('Failed to process page', err);
123
130
  sendError(err, res);
124
131
  });
132
+ queue.on('event', (event) => {
133
+ if (event.type === 'FILE_CHUNK') {
134
+ return;
135
+ }
136
+ sendEvent(res, event);
137
+ });
125
138
  await queue.addPrompt(aiRequest, conversationId, true);
126
139
  await queue.wait();
127
140
  await Promise.allSettled(promises);
@@ -200,8 +213,11 @@ router.post('/:handle/ui/iterative', async (req, res) => {
200
213
  pageQueue.cancel();
201
214
  });
202
215
  pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
203
- pageQueue.on('event', (screenData) => {
204
- sendEvent(res, screenData);
216
+ pageQueue.on('event', (event) => {
217
+ if (event.type === 'FILE_CHUNK') {
218
+ return;
219
+ }
220
+ sendEvent(res, event);
205
221
  });
206
222
  pageQueue.on('error', (err) => {
207
223
  console.error('Failed to process page', err);
@@ -353,8 +369,11 @@ router.post('/:handle/ui', async (req, res) => {
353
369
  queue.cancel();
354
370
  });
355
371
  queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
356
- queue.on('event', (screenData) => {
357
- sendEvent(res, screenData);
372
+ queue.on('event', (event) => {
373
+ if (event.type === 'FILE_CHUNK') {
374
+ return;
375
+ }
376
+ sendEvent(res, event);
358
377
  });
359
378
  queue.on('error', (err) => {
360
379
  console.error('Failed to process page', err);
@@ -410,10 +429,11 @@ router.post('/ui/edit', async (req, res) => {
410
429
  return promise;
411
430
  }
412
431
  });
413
- queue.on('event', (data) => {
414
- if (data.type === 'FILE_START' || data.type === 'FILE_DONE' || data.type === 'FILE_STATE') {
415
- sendEvent(res, data);
432
+ queue.on('event', (event) => {
433
+ if (event.type === 'FILE_CHUNK') {
434
+ return;
416
435
  }
436
+ sendEvent(res, event);
417
437
  });
418
438
  queue.on('error', (err) => {
419
439
  console.error('Failed to process page', err);
@@ -490,9 +510,13 @@ async function handleAll(req, res) {
490
510
  onRequestAborted(req, res, () => {
491
511
  metaStream.abort();
492
512
  });
493
- res.set('Content-Type', 'application/x-ndjson');
494
- res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
495
- res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
513
+ // We check if the headers have been sent, because we might have already sent some data
514
+ // before this function is called
515
+ if (!res.headersSent) {
516
+ res.set('Content-Type', 'application/x-ndjson');
517
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
518
+ res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
519
+ }
496
520
  let currentPhase = events_1.StormEventPhaseType.META;
497
521
  // Helper to avoid sending the plan multiple times in a row
498
522
  const sendUpdatedPlan = lodash_1.default.debounce(sendDefinitions, 50, { maxWait: 200 });
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
3
  import { Page, StormEventPageUrl } from './events';
3
4
  export declare const STORM_ID = "storm";
@@ -65,7 +66,7 @@ declare class StormClient {
65
66
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
66
67
  createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
67
68
  createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
68
- voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
69
+ voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
69
70
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
70
71
  vote: -1 | 0 | 1;
71
72
  }>;
@@ -13,22 +13,8 @@ const utils_1 = require("../utils/utils");
13
13
  const promises_1 = __importDefault(require("node:readline/promises"));
14
14
  const node_stream_1 = require("node:stream");
15
15
  const stream_1 = require("./stream");
16
- const undici_1 = require("undici");
17
- // Will only retry on error codes and GET requests by default
18
- // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
19
- const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
20
- methods: [
21
- // Added methods ↓ (not idempotent), but we want to retry on POST:
22
- 'POST',
23
- // defaults below
24
- 'GET',
25
- 'HEAD',
26
- 'OPTIONS',
27
- 'PUT',
28
- 'DELETE',
29
- 'TRACE',
30
- ],
31
- });
16
+ const fetch_retry_1 = __importDefault(require("fetch-retry"));
17
+ const fetchWithRetries = (0, fetch_retry_1.default)(global.fetch, { retries: 5, retryDelay: 10 });
32
18
  exports.STORM_ID = 'storm';
33
19
  exports.ConversationIdHeader = 'Conversation-Id';
34
20
  class StormClient {
@@ -54,7 +40,6 @@ class StormClient {
54
40
  method: method,
55
41
  body: JSON.stringify(body),
56
42
  headers,
57
- dispatcher: retryAgent,
58
43
  };
59
44
  }
60
45
  async send(path, body, method = 'POST') {
@@ -65,7 +50,7 @@ class StormClient {
65
50
  });
66
51
  const abort = new AbortController();
67
52
  options.signal = abort.signal;
68
- const response = await (0, undici_1.fetch)(options.url, options);
53
+ const response = await fetchWithRetries(options.url, options);
69
54
  if (response.status !== 200) {
70
55
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
71
56
  }
@@ -138,19 +123,19 @@ class StormClient {
138
123
  prompt: JSON.stringify({ topic, vote, mainConversationId }),
139
124
  conversationId,
140
125
  });
141
- return (0, undici_1.fetch)(options.url, options);
126
+ return fetch(options.url, options);
142
127
  }
143
128
  async getVoteUIPage(topic, conversationId, mainConversationId) {
144
129
  const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
145
130
  prompt: JSON.stringify({ topic, mainConversationId }),
146
131
  conversationId,
147
132
  });
148
- const response = await (0, undici_1.fetch)(options.url, options);
133
+ const response = await fetch(options.url, options);
149
134
  return response.json();
150
135
  }
151
136
  async implementAPIClients(prompt) {
152
137
  const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
153
- const response = await (0, undici_1.fetch)(u, {
138
+ const response = await fetch(u, {
154
139
  method: 'POST',
155
140
  body: JSON.stringify({
156
141
  fileName: prompt.fileName,
@@ -162,7 +147,7 @@ class StormClient {
162
147
  }
163
148
  async generatePrompt(pages) {
164
149
  const u = `${this._baseUrl}/v2/ui/prompt`;
165
- const response = await (0, undici_1.fetch)(u, {
150
+ const response = await fetch(u, {
166
151
  method: 'POST',
167
152
  body: JSON.stringify({
168
153
  pages: pages,
@@ -235,7 +220,7 @@ class StormClient {
235
220
  prompt: '',
236
221
  conversationId: conversationId,
237
222
  });
238
- const response = await (0, undici_1.fetch)(options.url, options);
223
+ const response = await fetch(options.url, options);
239
224
  return response.text();
240
225
  }
241
226
  }
@@ -271,6 +271,8 @@ export interface StormEventDefinitionChange {
271
271
  payload: StormDefinitions;
272
272
  }
273
273
  export declare enum StormEventPhaseType {
274
+ IMPLEMENT_APIS = "IMPLEMENT_APIS",// Implement APIs in the html pages
275
+ COMPOSE_SYSTEM_PROMPT = "COMPOSE_SYSTEM_PROMPT",// Compose system prompt for bottom-up approach
274
276
  META = "META",
275
277
  DEFINITIONS = "DEFINITIONS",
276
278
  IMPLEMENTATION = "IMPLEMENTATION",
@@ -11,6 +11,8 @@ var StormEventBlockStatusType;
11
11
  })(StormEventBlockStatusType || (exports.StormEventBlockStatusType = StormEventBlockStatusType = {}));
12
12
  var StormEventPhaseType;
13
13
  (function (StormEventPhaseType) {
14
+ StormEventPhaseType["IMPLEMENT_APIS"] = "IMPLEMENT_APIS";
15
+ StormEventPhaseType["COMPOSE_SYSTEM_PROMPT"] = "COMPOSE_SYSTEM_PROMPT";
14
16
  StormEventPhaseType["META"] = "META";
15
17
  StormEventPhaseType["DEFINITIONS"] = "DEFINITIONS";
16
18
  StormEventPhaseType["IMPLEMENTATION"] = "IMPLEMENTATION";
@@ -72,6 +72,10 @@ router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
72
72
  const systemId = req.params.systemId;
73
73
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
74
74
  const destDir = (0, page_utils_1.getSystemBaseImplDir)(systemId);
75
+ res.set('Content-Type', 'application/x-ndjson');
76
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
77
+ res.set(stormClient_1.ConversationIdHeader, systemId);
78
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.IMPLEMENT_APIS));
75
79
  await (0, utils_1.copyDirectory)(srcDir, destDir, async (fileName, content) => {
76
80
  const result = await stormClient_1.stormClient.implementAPIClients({
77
81
  content: content,
@@ -79,8 +83,11 @@ router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
79
83
  });
80
84
  return result;
81
85
  });
86
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.IMPLEMENT_APIS));
87
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
82
88
  const pages = (0, utils_1.readPages)(destDir);
83
89
  const prompt = await stormClient_1.stormClient.generatePrompt(pages);
90
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
84
91
  req.query.systemId = systemId;
85
92
  const promptRequest = {
86
93
  prompt: prompt,
@@ -122,6 +129,12 @@ router.post('/ui/screen', async (req, res) => {
122
129
  console.error('Failed to process page', err);
123
130
  sendError(err, res);
124
131
  });
132
+ queue.on('event', (event) => {
133
+ if (event.type === 'FILE_CHUNK') {
134
+ return;
135
+ }
136
+ sendEvent(res, event);
137
+ });
125
138
  await queue.addPrompt(aiRequest, conversationId, true);
126
139
  await queue.wait();
127
140
  await Promise.allSettled(promises);
@@ -200,8 +213,11 @@ router.post('/:handle/ui/iterative', async (req, res) => {
200
213
  pageQueue.cancel();
201
214
  });
202
215
  pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
203
- pageQueue.on('event', (screenData) => {
204
- sendEvent(res, screenData);
216
+ pageQueue.on('event', (event) => {
217
+ if (event.type === 'FILE_CHUNK') {
218
+ return;
219
+ }
220
+ sendEvent(res, event);
205
221
  });
206
222
  pageQueue.on('error', (err) => {
207
223
  console.error('Failed to process page', err);
@@ -353,8 +369,11 @@ router.post('/:handle/ui', async (req, res) => {
353
369
  queue.cancel();
354
370
  });
355
371
  queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
356
- queue.on('event', (screenData) => {
357
- sendEvent(res, screenData);
372
+ queue.on('event', (event) => {
373
+ if (event.type === 'FILE_CHUNK') {
374
+ return;
375
+ }
376
+ sendEvent(res, event);
358
377
  });
359
378
  queue.on('error', (err) => {
360
379
  console.error('Failed to process page', err);
@@ -410,10 +429,11 @@ router.post('/ui/edit', async (req, res) => {
410
429
  return promise;
411
430
  }
412
431
  });
413
- queue.on('event', (data) => {
414
- if (data.type === 'FILE_START' || data.type === 'FILE_DONE' || data.type === 'FILE_STATE') {
415
- sendEvent(res, data);
432
+ queue.on('event', (event) => {
433
+ if (event.type === 'FILE_CHUNK') {
434
+ return;
416
435
  }
436
+ sendEvent(res, event);
417
437
  });
418
438
  queue.on('error', (err) => {
419
439
  console.error('Failed to process page', err);
@@ -490,9 +510,13 @@ async function handleAll(req, res) {
490
510
  onRequestAborted(req, res, () => {
491
511
  metaStream.abort();
492
512
  });
493
- res.set('Content-Type', 'application/x-ndjson');
494
- res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
495
- res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
513
+ // We check if the headers have been sent, because we might have already sent some data
514
+ // before this function is called
515
+ if (!res.headersSent) {
516
+ res.set('Content-Type', 'application/x-ndjson');
517
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
518
+ res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
519
+ }
496
520
  let currentPhase = events_1.StormEventPhaseType.META;
497
521
  // Helper to avoid sending the plan multiple times in a row
498
522
  const sendUpdatedPlan = lodash_1.default.debounce(sendDefinitions, 50, { maxWait: 200 });
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
3
  import { Page, StormEventPageUrl } from './events';
3
4
  export declare const STORM_ID = "storm";
@@ -65,7 +66,7 @@ declare class StormClient {
65
66
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
66
67
  createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
67
68
  createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
68
- voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
69
+ voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
69
70
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
70
71
  vote: -1 | 0 | 1;
71
72
  }>;
@@ -13,22 +13,8 @@ const utils_1 = require("../utils/utils");
13
13
  const promises_1 = __importDefault(require("node:readline/promises"));
14
14
  const node_stream_1 = require("node:stream");
15
15
  const stream_1 = require("./stream");
16
- const undici_1 = require("undici");
17
- // Will only retry on error codes and GET requests by default
18
- // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
19
- const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
20
- methods: [
21
- // Added methods ↓ (not idempotent), but we want to retry on POST:
22
- 'POST',
23
- // defaults below
24
- 'GET',
25
- 'HEAD',
26
- 'OPTIONS',
27
- 'PUT',
28
- 'DELETE',
29
- 'TRACE',
30
- ],
31
- });
16
+ const fetch_retry_1 = __importDefault(require("fetch-retry"));
17
+ const fetchWithRetries = (0, fetch_retry_1.default)(global.fetch, { retries: 5, retryDelay: 10 });
32
18
  exports.STORM_ID = 'storm';
33
19
  exports.ConversationIdHeader = 'Conversation-Id';
34
20
  class StormClient {
@@ -54,7 +40,6 @@ class StormClient {
54
40
  method: method,
55
41
  body: JSON.stringify(body),
56
42
  headers,
57
- dispatcher: retryAgent,
58
43
  };
59
44
  }
60
45
  async send(path, body, method = 'POST') {
@@ -65,7 +50,7 @@ class StormClient {
65
50
  });
66
51
  const abort = new AbortController();
67
52
  options.signal = abort.signal;
68
- const response = await (0, undici_1.fetch)(options.url, options);
53
+ const response = await fetchWithRetries(options.url, options);
69
54
  if (response.status !== 200) {
70
55
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
71
56
  }
@@ -138,19 +123,19 @@ class StormClient {
138
123
  prompt: JSON.stringify({ topic, vote, mainConversationId }),
139
124
  conversationId,
140
125
  });
141
- return (0, undici_1.fetch)(options.url, options);
126
+ return fetch(options.url, options);
142
127
  }
143
128
  async getVoteUIPage(topic, conversationId, mainConversationId) {
144
129
  const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
145
130
  prompt: JSON.stringify({ topic, mainConversationId }),
146
131
  conversationId,
147
132
  });
148
- const response = await (0, undici_1.fetch)(options.url, options);
133
+ const response = await fetch(options.url, options);
149
134
  return response.json();
150
135
  }
151
136
  async implementAPIClients(prompt) {
152
137
  const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
153
- const response = await (0, undici_1.fetch)(u, {
138
+ const response = await fetch(u, {
154
139
  method: 'POST',
155
140
  body: JSON.stringify({
156
141
  fileName: prompt.fileName,
@@ -162,7 +147,7 @@ class StormClient {
162
147
  }
163
148
  async generatePrompt(pages) {
164
149
  const u = `${this._baseUrl}/v2/ui/prompt`;
165
- const response = await (0, undici_1.fetch)(u, {
150
+ const response = await fetch(u, {
166
151
  method: 'POST',
167
152
  body: JSON.stringify({
168
153
  pages: pages,
@@ -235,7 +220,7 @@ class StormClient {
235
220
  prompt: '',
236
221
  conversationId: conversationId,
237
222
  });
238
- const response = await (0, undici_1.fetch)(options.url, options);
223
+ const response = await fetch(options.url, options);
239
224
  return response.text();
240
225
  }
241
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.70.12",
3
+ "version": "0.71.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -70,6 +70,7 @@
70
70
  "download-git-repo": "^3.0.2",
71
71
  "express": "4.17.1",
72
72
  "express-promise-router": "^4.1.1",
73
+ "fetch-retry": "^6.0.0",
73
74
  "fs-extra": "^11.1.0",
74
75
  "glob": "^7.1.6",
75
76
  "gunzip-maybe": "^1.4.2",
@@ -88,7 +89,6 @@
88
89
  "stream-json": "^1.8.0",
89
90
  "tar-stream": "^3.1.6",
90
91
  "typescript": "^5.1.6",
91
- "undici": "^6.19.8",
92
92
  "uuid": "^9.0.1",
93
93
  "yaml": "^1.6.0"
94
94
  },
@@ -259,7 +259,7 @@ export class PageQueue extends EventEmitter {
259
259
  // Add safeguard to avoid generating images for nonsense URLs
260
260
  // Sometimes we get entries for Base URLs that will then cause issues on the filesystem
261
261
  // Example: https://www.kapeta.com/images/
262
- const mimeType = mimetypes.lookup(prompt.url) as string | false;
262
+ const mimeType = mimetypes.lookup(prompt.url);
263
263
  if (!mimeType || !mimeType.startsWith('image/')) {
264
264
  console.warn('Skipping image reference of type %s for url %s', mimeType, prompt.url);
265
265
  return;
@@ -329,6 +329,8 @@ export interface StormEventDefinitionChange {
329
329
  }
330
330
 
331
331
  export enum StormEventPhaseType {
332
+ IMPLEMENT_APIS = 'IMPLEMENT_APIS', // Implement APIs in the html pages
333
+ COMPOSE_SYSTEM_PROMPT = 'COMPOSE_SYSTEM_PROMPT', // Compose system prompt for bottom-up approach
332
334
  META = 'META',
333
335
  DEFINITIONS = 'DEFINITIONS',
334
336
  IMPLEMENTATION = 'IMPLEMENTATION',
@@ -105,6 +105,12 @@ router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest
105
105
  const srcDir = getSystemBaseDir(systemId);
106
106
  const destDir = getSystemBaseImplDir(systemId);
107
107
 
108
+ res.set('Content-Type', 'application/x-ndjson');
109
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
110
+ res.set(ConversationIdHeader, systemId);
111
+
112
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENT_APIS));
113
+
108
114
  await copyDirectory(srcDir, destDir, async (fileName, content) => {
109
115
  const result = await stormClient.implementAPIClients({
110
116
  content: content,
@@ -113,9 +119,15 @@ router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest
113
119
  return result;
114
120
  });
115
121
 
122
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.IMPLEMENT_APIS));
123
+
124
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
125
+
116
126
  const pages = readPages(destDir);
117
127
  const prompt = await stormClient.generatePrompt(pages);
118
128
 
129
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.COMPOSE_SYSTEM_PROMPT));
130
+
119
131
  req.query.systemId = systemId;
120
132
  const promptRequest: BasePromptRequest = {
121
133
  prompt: prompt,
@@ -169,6 +181,13 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
169
181
  sendError(err as any, res);
170
182
  });
171
183
 
184
+ queue.on('event', (event: StormEvent) => {
185
+ if (event.type === 'FILE_CHUNK') {
186
+ return;
187
+ }
188
+ sendEvent(res, event);
189
+ });
190
+
172
191
  await queue.addPrompt(aiRequest, conversationId, true);
173
192
 
174
193
  await queue.wait();
@@ -262,8 +281,11 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
262
281
  sendPageEvent(landingPagesStream.getConversationId(), screenData, res)
263
282
  );
264
283
 
265
- pageQueue.on('event', (screenData: StormEvent) => {
266
- sendEvent(res, screenData);
284
+ pageQueue.on('event', (event: StormEvent) => {
285
+ if (event.type === 'FILE_CHUNK') {
286
+ return;
287
+ }
288
+ sendEvent(res, event);
267
289
  });
268
290
 
269
291
  pageQueue.on('error', (err) => {
@@ -448,8 +470,11 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
448
470
 
449
471
  queue.on('page', (screenData: StormEventPage) => sendPageEvent(outerConversationId, screenData, res));
450
472
 
451
- queue.on('event', (screenData: StormEvent) => {
452
- sendEvent(res, screenData);
473
+ queue.on('event', (event: StormEvent) => {
474
+ if (event.type === 'FILE_CHUNK') {
475
+ return;
476
+ }
477
+ sendEvent(res, event);
453
478
  });
454
479
 
455
480
  queue.on('error', (err) => {
@@ -515,10 +540,11 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
515
540
  }
516
541
  });
517
542
 
518
- queue.on('event', (data) => {
519
- if (data.type === 'FILE_START' || data.type === 'FILE_DONE' || data.type === 'FILE_STATE') {
520
- sendEvent(res, data);
543
+ queue.on('event', (event) => {
544
+ if (event.type === 'FILE_CHUNK') {
545
+ return;
521
546
  }
547
+ sendEvent(res, event);
522
548
  });
523
549
 
524
550
  queue.on('error', (err) => {
@@ -612,9 +638,13 @@ async function handleAll(req: KapetaBodyRequest, res: Response) {
612
638
  metaStream.abort();
613
639
  });
614
640
 
615
- res.set('Content-Type', 'application/x-ndjson');
616
- res.set('Access-Control-Expose-Headers', ConversationIdHeader);
617
- res.set(ConversationIdHeader, metaStream.getConversationId());
641
+ // We check if the headers have been sent, because we might have already sent some data
642
+ // before this function is called
643
+ if (!res.headersSent) {
644
+ res.set('Content-Type', 'application/x-ndjson');
645
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
646
+ res.set(ConversationIdHeader, metaStream.getConversationId());
647
+ }
618
648
 
619
649
  let currentPhase = StormEventPhaseType.META;
620
650
 
@@ -16,23 +16,8 @@ import {
16
16
  StormUIListPrompt,
17
17
  } from './stream';
18
18
  import { Page, StormEventPageUrl } from './events';
19
- import { fetch, RequestInit, Agent, RetryAgent } from 'undici';
20
-
21
- // Will only retry on error codes and GET requests by default
22
- // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
23
- const retryAgent = new RetryAgent(new Agent(), {
24
- methods: [
25
- // Added methods ↓ (not idempotent), but we want to retry on POST:
26
- 'POST',
27
- // defaults below
28
- 'GET',
29
- 'HEAD',
30
- 'OPTIONS',
31
- 'PUT',
32
- 'DELETE',
33
- 'TRACE',
34
- ],
35
- });
19
+ import createFetch from 'fetch-retry';
20
+ const fetchWithRetries = createFetch(global.fetch, { retries: 5, retryDelay: 10 });
36
21
 
37
22
  export const STORM_ID = 'storm';
38
23
 
@@ -129,7 +114,6 @@ class StormClient {
129
114
  method: method,
130
115
  body: JSON.stringify(body),
131
116
  headers,
132
- dispatcher: retryAgent,
133
117
  };
134
118
  }
135
119
 
@@ -148,7 +132,7 @@ class StormClient {
148
132
  const abort = new AbortController();
149
133
  options.signal = abort.signal;
150
134
 
151
- const response = await fetch(options.url, options);
135
+ const response = await fetchWithRetries(options.url, options);
152
136
 
153
137
  if (response.status !== 200) {
154
138
  throw new Error(