@zodic/shared 0.0.165 → 0.0.167

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/app/backend.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from '../utils';
2
2
  export * from './base';
3
+ export * from './durable';
3
4
  export * from './services';
4
5
  export * from './workflow';
@@ -0,0 +1 @@
1
+ export * from './ConceptNameDO';
@@ -31,20 +31,15 @@ export class ConceptService {
31
31
 
32
32
  const kvStore = this.context.kvConceptsStore();
33
33
  const kvFailuresStore = this.context.kvConceptFailuresStore();
34
- const kvNamesStore = this.context.kvConceptNamesStore(); // ✅ New KV for storing names
34
+ const kvNamesStore = this.context.kvConceptNamesStore(); // ✅ New Durable Object
35
35
  const kvKeyEN = buildConceptKVKey('en-us', conceptSlug, combinationString);
36
36
  const kvKeyPT = buildConceptKVKey('pt-br', conceptSlug, combinationString);
37
37
  const failureKey = `failures:basic-info:${conceptSlug}:${combinationString}`;
38
38
 
39
- const namesKeyEN = `${conceptSlug}:en-us`;
40
- const namesKeyPT = `${conceptSlug}:pt-br`;
41
- const reportKey = `report:${conceptSlug}`; // ✅ New tracking key
42
-
43
39
  // ✅ Check if data already exists
44
40
  if (!override) {
45
41
  const existingEN = await this.getKVConcept(kvKeyEN);
46
42
  const existingPT = await this.getKVConcept(kvKeyPT);
47
-
48
43
  if (
49
44
  existingEN.name &&
50
45
  existingEN.description &&
@@ -56,7 +51,7 @@ export class ConceptService {
56
51
  existingPT.poem.length > 0
57
52
  ) {
58
53
  console.log(
59
- `⚡ Basic info already exists for ${conceptSlug}, combination: ${combinationString}. Skipping.`
54
+ `⚡ Basic info already exists for ${conceptSlug}, skipping.`
60
55
  );
61
56
  return;
62
57
  }
@@ -71,75 +66,82 @@ export class ConceptService {
71
66
  attempts++;
72
67
  console.log(`🔄 Attempt ${attempts} to generate basic info...`);
73
68
 
74
- // ✅ Fetch the full name lists
75
- let allNamesEN = JSON.parse(
76
- (await kvNamesStore.get(namesKeyEN)) || '[]'
77
- ) as string[];
78
- let allNamesPT = JSON.parse(
79
- (await kvNamesStore.get(namesKeyPT)) || '[]'
80
- ) as string[];
69
+ // ✅ Fetch the latest name list **directly from Durable Object**
70
+ const response = await fetch(
71
+ `${this.context.env.API_BASE_URL}/concept-names/${conceptSlug}`
72
+ );
73
+ let allNamesEN: string[] = [];
74
+ let allNamesPT: string[] = [];
75
+ if (response.ok) {
76
+ const data = (await response.json()) as {
77
+ 'en-us': string[];
78
+ 'pt-br': string[];
79
+ };
80
+ allNamesEN = data['en-us'] || [];
81
+ allNamesPT = data['pt-br'] || [];
82
+ }
81
83
 
82
- // Get only the last 50 names for AI context
83
- const recentNamesEN = allNamesEN.slice(-144);
84
+ const recentNamesEN = allNamesEN.slice(-144); // 🔥 Only pass the last 144 names
84
85
  const recentNamesPT = allNamesPT.slice(-144);
85
86
 
86
- // ✅ Build the messages to request content, ensuring uniqueness
87
+ // ✅ Generate content ensuring uniqueness
87
88
  const messages = this.context
88
89
  .buildLLMMessages()
89
90
  .generateConceptBasicInfo({
90
91
  combination: combinationString,
91
92
  conceptSlug,
92
- existingNames: recentNamesEN, // 🔥 Pass only recent names
93
+ existingNames: recentNamesEN,
93
94
  });
94
95
 
95
- // Call ChatGPT API
96
- let response = await this.context.api().callTogether.single(messages, {});
97
- if (!response) {
96
+ let aiResponse = await this.context
97
+ .api()
98
+ .callTogether.single(messages, {});
99
+ if (!aiResponse) {
98
100
  throw new Error(`❌ AI returned an empty response`);
99
101
  }
100
102
 
101
103
  phase = 'cleaning';
102
- response = this.cleanAIResponse(response);
104
+ aiResponse = this.cleanAIResponse(aiResponse);
103
105
 
104
106
  phase = 'parsing';
105
107
 
106
- // ✅ Parse response for both languages
107
108
  let { nameEN, descriptionEN, poemEN, namePT, descriptionPT, poemPT } =
108
- this.parseBasicInfoResponse(response, conceptSlug);
109
+ this.parseBasicInfoResponse(aiResponse, conceptSlug);
109
110
 
110
- // 🔥 Check if generated English name is unique
111
- if (allNamesEN.includes(nameEN)) {
112
- console.warn(`⚠️ Duplicate Crown Name Detected: "${nameEN}"`);
111
+ // Check if generated names are unique **right after parsing**
112
+ if (allNamesEN.includes(nameEN) || allNamesPT.includes(namePT)) {
113
+ console.warn(
114
+ `⚠️ Duplicate Name Detected: "${nameEN}" or "${namePT}"`
115
+ );
113
116
  if (attempts >= maxAttempts) {
114
117
  throw new Error(
115
- `🚨 Could not generate a unique English name after ${maxAttempts} attempts.`
118
+ `🚨 Could not generate a unique name after ${maxAttempts} attempts.`
116
119
  );
117
120
  }
118
- console.log('🔁 Retrying due to duplicate English name...');
121
+ console.log('🔁 Retrying due to duplicate name...');
119
122
  continue;
120
123
  }
121
124
 
122
- // 🔥 Check if generated Portuguese name is unique
123
- if (allNamesPT.includes(namePT)) {
124
- console.warn(`⚠️ Duplicate Portuguese Name Detected: "${namePT}"`);
125
- if (attempts >= maxAttempts) {
126
- throw new Error(
127
- `🚨 Could not generate a unique Portuguese name after ${maxAttempts} attempts.`
128
- );
125
+ // **Immediately update Durable Object with the new name**
126
+ await fetch(
127
+ `${this.context.env.API_BASE_URL}/concept-names/${conceptSlug}/add`,
128
+ {
129
+ method: 'POST',
130
+ body: JSON.stringify({ language: 'en-us', name: nameEN }),
131
+ headers: { 'Content-Type': 'application/json' },
129
132
  }
130
- console.log('🔁 Retrying due to duplicate Portuguese name...');
131
- continue;
132
- }
133
-
134
- // ✅ Append new names to the full list
135
- allNamesEN.push(nameEN);
136
- allNamesPT.push(namePT);
133
+ );
137
134
 
138
- // ✅ Store updated name lists in KV
139
- await kvNamesStore.put(namesKeyEN, JSON.stringify(allNamesEN));
140
- await kvNamesStore.put(namesKeyPT, JSON.stringify(allNamesPT));
135
+ await fetch(
136
+ `${this.context.env.API_BASE_URL}/concept-names/${conceptSlug}/add`,
137
+ {
138
+ method: 'POST',
139
+ body: JSON.stringify({ language: 'pt-br', name: namePT }),
140
+ headers: { 'Content-Type': 'application/json' },
141
+ }
142
+ );
141
143
 
142
- // 🌍 English version
144
+ // Store the generated basic info in KV
143
145
  const conceptEN = await this.getKVConcept(kvKeyEN);
144
146
  Object.assign(conceptEN, {
145
147
  name: nameEN,
@@ -149,7 +151,6 @@ export class ConceptService {
149
151
  });
150
152
  await kvStore.put(kvKeyEN, JSON.stringify(conceptEN));
151
153
 
152
- // 🇧🇷 Portuguese version
153
154
  const conceptPT = await this.getKVConcept(kvKeyPT);
154
155
  Object.assign(conceptPT, {
155
156
  name: namePT,
@@ -160,19 +161,7 @@ export class ConceptService {
160
161
  await kvStore.put(kvKeyPT, JSON.stringify(conceptPT));
161
162
 
162
163
  console.log(
163
- `✅ Basic info stored for ${conceptSlug}, combination: ${combinationString}, in both languages.`
164
- );
165
-
166
- // ✅ Increment report tracking successful inserts
167
- let currentCount = parseInt(
168
- (await kvNamesStore.get(reportKey)) || '0',
169
- 10
170
- );
171
- currentCount += 1;
172
- await kvNamesStore.put(reportKey, currentCount.toString());
173
-
174
- console.log(
175
- `📊 Updated progress for ${conceptSlug}: ${currentCount} successful inserts.`
164
+ `✅ Stored basic info for ${conceptSlug}, combination: ${combinationString}.`
176
165
  );
177
166
 
178
167
  return; // ✅ Exit loop if successful
@@ -181,29 +170,12 @@ export class ConceptService {
181
170
  `❌ Attempt ${attempts} failed at phase: ${phase}`,
182
171
  (error as Error).message
183
172
  );
184
-
185
- // ✅ Store failure details in KV for manual review
186
- await kvFailuresStore.put(
187
- failureKey,
188
- JSON.stringify({
189
- error: (error as Error).message,
190
- attempt: attempts,
191
- phase,
192
- conceptSlug,
193
- combinationString,
194
- timestamp: new Date().toISOString(),
195
- })
196
- );
197
-
198
173
  if (attempts >= maxAttempts) {
199
- console.error(
200
- `🚨 All ${maxAttempts} attempts failed during ${phase}. Logged failure.`
201
- );
174
+ console.error(`🚨 All ${maxAttempts} attempts failed.`);
202
175
  throw new Error(
203
- `Failed to generate basic info after ${maxAttempts} attempts`
176
+ `Failed to generate basic info after ${maxAttempts} attempts.`
204
177
  );
205
178
  }
206
-
207
179
  console.log('🔁 Retrying...');
208
180
  await new Promise((resolve) => setTimeout(resolve, 500)); // ⏳ Small delay before retrying
209
181
  }
@@ -212,7 +184,7 @@ export class ConceptService {
212
184
 
213
185
  private cleanAIResponse(response: string): string {
214
186
  console.log('🧼 Cleaning AI Response...');
215
-
187
+
216
188
  return response
217
189
  .replace(/\*/g, '') // ✅ Remove asterisks
218
190
  .replace(/#/g, '') // ✅ Remove hashtags
@@ -232,23 +204,23 @@ export class ConceptService {
232
204
  poemPT: string[];
233
205
  } {
234
206
  console.log('📌 Parsing basic info response from ChatGPT:', response);
235
-
207
+
236
208
  // ✅ More flexible regex to handle variations in formatting
237
209
  const enMatch = response.match(
238
210
  /EN:\s*(?:•\s*)?Name:\s*(.+?)\s*(?:•\s*)?Description:\s*([\s\S]+?)\s*(?:•\s*)?Poetic Passage:\s*([\s\S]+?)\s*(?=PT:|$)/
239
211
  );
240
212
  const ptMatch = response.match(
241
- /PT:\s*(?:•\s*)?Nome:\s*(.+?)\s*(?:•\s*)?Descrição:\s*([\s\S]+?)\s*(?:•\s*)?Passagem Poética:\s*([\s\S]+)/
213
+ /PT:\s*(?:•\s*)?Nome:\s*(.+?)\s*(?:•\s*)?Descrição:\s*([\s\S]+?)\s*(?:•\s*)?Passagem Poética:\s*([\s\S]+)/
242
214
  );
243
-
215
+
244
216
  if (!enMatch || !ptMatch) {
245
217
  console.error('❌ Invalid basic info response format:', response);
246
218
  throw new Error('Invalid basic info response format');
247
219
  }
248
-
220
+
249
221
  // ✅ Function to clean text (removes leading/trailing spaces & unwanted symbols)
250
222
  const cleanText = (text: string) => text.trim().replace(/\*/g, '');
251
-
223
+
252
224
  // ✅ Parse and clean extracted content
253
225
  const nameEN = cleanText(enMatch[1]);
254
226
  const descriptionEN = cleanText(enMatch[2]);
@@ -256,37 +228,45 @@ export class ConceptService {
256
228
  .trim()
257
229
  .split(/\n+/)
258
230
  .map((line) => cleanText(line));
259
-
231
+
260
232
  const namePT = cleanText(ptMatch[1]);
261
233
  const descriptionPT = cleanText(ptMatch[2]);
262
234
  const poemPT = ptMatch[3]
263
235
  .trim()
264
236
  .split(/\n+/)
265
237
  .map((line) => cleanText(line));
266
-
238
+
267
239
  // ✅ Determine appropriate replacement text based on conceptSlug
268
- const conceptPlaceholder = `this ${conceptSlug.charAt(0).toUpperCase() + conceptSlug.slice(1)}`;
269
- const conceptPlaceholderCapitalized =
270
- `This ${conceptSlug.charAt(0).toUpperCase() + conceptSlug.slice(1)}`;
271
-
240
+ const conceptPlaceholder = `this ${
241
+ conceptSlug.charAt(0).toUpperCase() + conceptSlug.slice(1)
242
+ }`;
243
+ const conceptPlaceholderCapitalized = `This ${
244
+ conceptSlug.charAt(0).toUpperCase() + conceptSlug.slice(1)
245
+ }`;
246
+
272
247
  // ✅ Function to replace occurrences of the name in a text while handling capitalization
273
248
  const replaceConceptName = (text: string, conceptName: string) => {
274
- return text.replace(
275
- new RegExp(`(?<=\\.\\s*|^)${conceptName}`, 'g'),
276
- conceptPlaceholderCapitalized
277
- ).replace(new RegExp(conceptName, 'g'), conceptPlaceholder);
249
+ return text
250
+ .replace(
251
+ new RegExp(`(?<=\\.\\s*|^)${conceptName}`, 'g'),
252
+ conceptPlaceholderCapitalized
253
+ )
254
+ .replace(new RegExp(conceptName, 'g'), conceptPlaceholder);
278
255
  };
279
-
256
+
280
257
  // ✅ Ensure the concept name is not repeated in the description
281
258
  const cleanedDescriptionEN = replaceConceptName(descriptionEN, nameEN);
282
259
  const cleanedDescriptionPT = replaceConceptName(descriptionPT, namePT);
283
-
260
+
284
261
  // ✅ If the name appears in the poem, trigger a retry
285
- if (poemEN.some((line) => line.includes(nameEN)) || poemPT.some((line) => line.includes(namePT))) {
262
+ if (
263
+ poemEN.some((line) => line.includes(nameEN)) ||
264
+ poemPT.some((line) => line.includes(namePT))
265
+ ) {
286
266
  console.error('❌ Concept name detected in poem, triggering a retry.');
287
267
  throw new Error('Concept name found in poem, regenerating response.');
288
268
  }
289
-
269
+
290
270
  console.log('✅ Successfully parsed and cleaned basic info:', {
291
271
  nameEN,
292
272
  descriptionEN: cleanedDescriptionEN,
@@ -295,7 +275,7 @@ export class ConceptService {
295
275
  descriptionPT: cleanedDescriptionPT,
296
276
  poemPT,
297
277
  });
298
-
278
+
299
279
  return {
300
280
  nameEN,
301
281
  descriptionEN: cleanedDescriptionEN,
@@ -317,31 +297,37 @@ export class ConceptService {
317
297
  console.log(
318
298
  `🚀 Generating Leonardo prompt for concept: ${conceptSlug}, combination: ${combinationString}`
319
299
  );
320
-
300
+
321
301
  // ✅ Build KV keys for both languages
322
302
  const kvKeyEN = buildConceptKVKey('en-us', conceptSlug, combinationString);
323
303
  const kvKeyPT = buildConceptKVKey('pt-br', conceptSlug, combinationString);
324
-
304
+
325
305
  // ✅ Retrieve existing KV data
326
306
  const conceptEN = await this.getKVConcept(kvKeyEN);
327
307
  const conceptPT = await this.getKVConcept(kvKeyPT);
328
-
308
+
329
309
  // ✅ Check if prompt already exists
330
310
  if (!override && conceptEN.leonardoPrompt && conceptPT.leonardoPrompt) {
331
- console.log(`⚡ Leonardo prompt already exists for ${conceptSlug}, skipping.`);
311
+ console.log(
312
+ `⚡ Leonardo prompt already exists for ${conceptSlug}, skipping.`
313
+ );
332
314
  return; // ✅ Skip regeneration
333
315
  }
334
-
316
+
335
317
  // ✅ Ensure basic info is present
336
318
  if (
337
- !conceptEN.name || !conceptEN.description || !conceptEN.poem ||
338
- !conceptPT.name || !conceptPT.description || !conceptPT.poem
319
+ !conceptEN.name ||
320
+ !conceptEN.description ||
321
+ !conceptEN.poem ||
322
+ !conceptPT.name ||
323
+ !conceptPT.description ||
324
+ !conceptPT.poem
339
325
  ) {
340
326
  throw new Error(
341
327
  `❌ Basic info must be populated before generating Leonardo prompt for concept: ${conceptSlug}`
342
328
  );
343
329
  }
344
-
330
+
345
331
  // ✅ Generate prompt request
346
332
  const messages = this.context
347
333
  .buildLLMMessages()
@@ -349,7 +335,7 @@ export class ConceptService {
349
335
  conceptSlug,
350
336
  combination: combinationString,
351
337
  });
352
-
338
+
353
339
  // ✅ Call ChatGPT API
354
340
  const response = await this.context.api().callTogether.single(messages, {});
355
341
  if (!response) {
@@ -357,16 +343,16 @@ export class ConceptService {
357
343
  `❌ Failed to generate Leonardo prompt for concept: ${conceptSlug}`
358
344
  );
359
345
  }
360
-
346
+
361
347
  // ✅ Store the generated prompt in both languages
362
348
  conceptEN.leonardoPrompt = response.trim();
363
349
  conceptPT.leonardoPrompt = response.trim();
364
-
350
+
365
351
  await Promise.all([
366
352
  this.context.kvConceptsStore().put(kvKeyEN, JSON.stringify(conceptEN)),
367
353
  this.context.kvConceptsStore().put(kvKeyPT, JSON.stringify(conceptPT)),
368
354
  ]);
369
-
355
+
370
356
  console.log(
371
357
  `✅ Leonardo prompt stored for concept: ${conceptSlug}, combination: ${combinationString}, in both languages.`
372
358
  );
@@ -501,16 +487,19 @@ export class ConceptService {
501
487
  structuredContentPT: StructuredConceptContent;
502
488
  } {
503
489
  console.log('📌 [START] Parsing structured content from ChatGPT response.');
504
-
490
+
505
491
  // ✅ Step 1: Clean artifacts before processing
506
492
  let cleanedResponse = response
507
493
  .replace(/[*#•]/g, '') // Remove bullet points, bold markers, headings
508
494
  .replace(/---+/g, '') // Remove dividers (like "---")
509
495
  .replace(/\n\s*\n/g, '\n\n') // Normalize multiple line breaks
510
496
  .trim();
511
-
512
- console.log('🔹 [CLEANED RESPONSE]:', cleanedResponse.slice(0, 500) + '...');
513
-
497
+
498
+ console.log(
499
+ '🔹 [CLEANED RESPONSE]:',
500
+ cleanedResponse.slice(0, 500) + '...'
501
+ );
502
+
514
503
  const sectionsEN = [
515
504
  'Core Identity',
516
505
  'Strengths and Challenges',
@@ -518,7 +507,7 @@ export class ConceptService {
518
507
  'Emotional Depth',
519
508
  'Vision and Aspirations',
520
509
  ];
521
-
510
+
522
511
  const sectionsPT = [
523
512
  'Identidade Essencial',
524
513
  'Forças e Desafios',
@@ -526,58 +515,76 @@ export class ConceptService {
526
515
  'Profundidade Emocional',
527
516
  'Visão e Aspirações',
528
517
  ];
529
-
518
+
530
519
  // ✅ Step 2: Extract English and Portuguese content separately
531
520
  const enMatches = cleanedResponse.match(/EN:\s*([\s\S]+?)\s*PT:/);
532
521
  const ptMatches = cleanedResponse.match(/PT:\s*([\s\S]+)/);
533
-
522
+
534
523
  if (!enMatches || !ptMatches) {
535
- console.error('❌ [ERROR] Missing English or Portuguese content in response.');
524
+ console.error(
525
+ '❌ [ERROR] Missing English or Portuguese content in response.'
526
+ );
536
527
  throw new Error('❌ Missing English or Portuguese content in response.');
537
528
  }
538
-
529
+
539
530
  const enContent = enMatches[1].trim();
540
531
  const ptContent = ptMatches[1].trim();
541
-
542
- console.log('✅ [MATCH SUCCESS] Extracted English Content:', enContent.slice(0, 500) + '...');
543
- console.log('✅ [MATCH SUCCESS] Extracted Portuguese Content:', ptContent.slice(0, 500) + '...');
544
-
532
+
533
+ console.log(
534
+ '✅ [MATCH SUCCESS] Extracted English Content:',
535
+ enContent.slice(0, 500) + '...'
536
+ );
537
+ console.log(
538
+ '✅ [MATCH SUCCESS] Extracted Portuguese Content:',
539
+ ptContent.slice(0, 500) + '...'
540
+ );
541
+
545
542
  // ✅ Step 3: Extract structured sections
546
543
  function extractSections(text: string, sectionTitles: string[]) {
547
544
  return sectionTitles.map((title) => {
548
545
  console.log(`🔍 [PROCESSING] Extracting section: "${title}"`);
549
-
546
+
550
547
  // ✅ Improved regex: Detects sections even if AI formatting changes
551
- const regex = new RegExp(`\\d+\\.\\s*${title}:\\s*([\\s\\S]+?)(?=\\n\\d+\\.\\s*[A-Z]|$)`);
548
+ const regex = new RegExp(
549
+ `\\d+\\.\\s*${title}:\\s*([\\s\\S]+?)(?=\\n\\d+\\.\\s*[A-Z]|$)`
550
+ );
552
551
  const match = text.match(regex);
553
-
552
+
554
553
  if (!match) {
555
554
  console.warn(`⚠️ [WARNING] Missing section: "${title}"`);
556
555
  return { type: 'section', title, content: ['❌ Section Missing'] };
557
556
  }
558
-
557
+
559
558
  // ✅ Split content into paragraphs and clean them
560
559
  const paragraphs = match[1]
561
560
  .trim()
562
561
  .split(/\n{2,}/) // Handles cases where paragraphs are separated by multiple new lines
563
562
  .map((p) => p.trim())
564
563
  .filter((p) => p.length > 0); // Removes empty paragraphs
565
-
564
+
566
565
  console.log(`✅ [EXTRACTED] "${title}" - Parsed Content:`, paragraphs);
567
-
566
+
568
567
  return { type: 'section', title, content: paragraphs };
569
568
  });
570
569
  }
571
-
570
+
572
571
  // ✅ Return parsed and cleaned structured content
573
572
  const structuredContentEN = extractSections(enContent, sectionsEN);
574
573
  const structuredContentPT = extractSections(ptContent, sectionsPT);
575
-
576
- console.log('🎯 [FINAL RESULT] Parsed English Content:', JSON.stringify(structuredContentEN, null, 2));
577
- console.log('🎯 [FINAL RESULT] Parsed Portuguese Content:', JSON.stringify(structuredContentPT, null, 2));
578
-
579
- console.log('✅ [COMPLETE] Structured content parsing finished successfully.');
580
-
574
+
575
+ console.log(
576
+ '🎯 [FINAL RESULT] Parsed English Content:',
577
+ JSON.stringify(structuredContentEN, null, 2)
578
+ );
579
+ console.log(
580
+ '🎯 [FINAL RESULT] Parsed Portuguese Content:',
581
+ JSON.stringify(structuredContentPT, null, 2)
582
+ );
583
+
584
+ console.log(
585
+ '✅ [COMPLETE] Structured content parsing finished successfully.'
586
+ );
587
+
581
588
  return {
582
589
  structuredContentEN,
583
590
  structuredContentPT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zodic/shared",
3
- "version": "0.0.165",
3
+ "version": "0.0.167",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -116,6 +116,7 @@ export type BackendBindings = Env &
116
116
  | 'PROMPT_GENERATE_ARCHETYPE_BASIC_INFO'
117
117
  | 'TOGETHER_API_KEY'
118
118
  | 'CONCEPT_NAMES_DO'
119
+ | 'API_BASE_URL'
119
120
  >;
120
121
 
121
122
  export type BackendCtx = Context<