@zodic/shared 0.0.368 → 0.0.370

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.
@@ -2,7 +2,7 @@ import {
2
2
  KVNamespaceListKey,
3
3
  KVNamespaceListResult,
4
4
  } from '@cloudflare/workers-types';
5
- import { and, eq, inArray, isNull, sql } from 'drizzle-orm';
5
+ import { and, eq, inArray, isNull } from 'drizzle-orm';
6
6
  import { drizzle } from 'drizzle-orm/d1';
7
7
  import { inject, injectable } from 'inversify';
8
8
  import 'reflect-metadata';
@@ -1118,430 +1118,24 @@ export class ConceptService {
1118
1118
  };
1119
1119
  }
1120
1120
 
1121
- async findDuplicateNamesForConcept(conceptSlug: Concept) {
1122
- console.log(
1123
- `🔍 Fetching duplicate names from KV_CONCEPT_CACHE for ${conceptSlug}...`
1124
- );
1125
-
1126
- const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
1127
-
1128
- // ✅ Fetch the cached concept name mappings
1129
- const nameMap: Record<string, string[]> =
1130
- (await kvCacheStore.get(
1131
- `cache:concepts:en-us:${conceptSlug}:0`,
1132
- 'json'
1133
- )) || {};
1134
-
1135
- let duplicateKeys: string[] = [];
1136
- let usedNamesEN = new Set<string>();
1137
- let usedNamesPT = new Set<string>();
1138
-
1139
- for (const [name, keys] of Object.entries(nameMap)) {
1140
- if (keys.length > 1) {
1141
- duplicateKeys.push(...keys.slice(1)); // ✅ Store all but the first occurrence
1142
- }
1143
- usedNamesEN.add(name);
1144
- }
1145
-
1146
- // ✅ Fetch PT-BR version from cache
1147
- const nameMapPT: Record<string, string[]> =
1148
- (await kvCacheStore.get(
1149
- `cache:concepts:pt-br:${conceptSlug}:0`,
1150
- 'json'
1151
- )) || {};
1152
-
1153
- for (const name of Object.keys(nameMapPT)) {
1154
- usedNamesPT.add(name);
1155
- }
1156
-
1157
- console.log(
1158
- `✅ Found ${duplicateKeys.length} duplicate keys for ${conceptSlug}.`
1159
- );
1160
-
1161
- return {
1162
- duplicateKeys, // ✅ List of keys that need new names
1163
- usedNamesEN, // ✅ Set of all used EN names
1164
- usedNamesPT, // ✅ Set of all used PT names
1165
- };
1166
- }
1167
-
1168
- async regenerateDuplicateNamesForConcept(
1169
- conceptSlug: Concept,
1170
- maxEntries?: number
1171
- ) {
1172
- console.log(
1173
- `🔄 [START] Regenerating duplicate names for concept: ${conceptSlug}...`
1174
- );
1175
- const startTime = Date.now();
1176
-
1177
- const db = drizzle(this.context.env.DB);
1178
-
1179
- // ✅ Step 1: Fetch duplicate entries filtered by conceptSlug
1180
- console.log(`📡 Fetching duplicate entries for concept: ${conceptSlug}...`);
1181
-
1182
- const duplicateEntries = await db
1183
- .select({
1184
- id: conceptsData.id,
1185
- name: conceptsData.name,
1186
- description: conceptsData.description,
1187
- combination: conceptsData.combination,
1188
- })
1189
- .from(conceptsData)
1190
- .where(
1191
- and(
1192
- eq(conceptsData.language, 'en-us'), // ✅ English only
1193
- eq(conceptsData.conceptSlug, conceptSlug), // ✅ Filter by concept
1194
- sql`
1195
- name IN (
1196
- SELECT name
1197
- FROM concepts_data
1198
- WHERE language = 'en-us' AND concept_slug = ${conceptSlug}
1199
- GROUP BY name
1200
- HAVING COUNT(*) > 1
1201
- )
1202
- `
1203
- )
1204
- )
1205
- .orderBy(conceptsData.name) // ✅ Order by name for better grouping
1206
- .limit(maxEntries || 50) // ✅ Apply optional limit
1207
- .execute();
1208
-
1209
- if (!duplicateEntries.length) {
1210
- console.log(`🎉 No duplicates found for ${conceptSlug}.`);
1211
- return {
1212
- message: `No duplicates detected for ${conceptSlug}.`,
1213
- remainingDuplicates: 0,
1214
- };
1215
- }
1216
-
1217
- console.log(
1218
- `🔍 Found ${duplicateEntries.length} duplicate entries to process.`
1219
- );
1220
-
1221
- // ✅ Group duplicates by name
1222
- const duplicateGroups = new Map<string, typeof duplicateEntries>();
1223
- for (const entry of duplicateEntries) {
1224
- if (!duplicateGroups.has(entry.name)) {
1225
- duplicateGroups.set(entry.name, []);
1226
- }
1227
- duplicateGroups.get(entry.name)!.push(entry);
1228
- }
1229
-
1230
- let recentDuplicates = new Set<string>(); // ✅ Track newly generated names
1231
- let updatedEntries = 0;
1232
- let skippedEntries = 0;
1233
- let failedEntries = 0;
1234
-
1235
- for (const [duplicateName, entries] of duplicateGroups.entries()) {
1236
- console.log(
1237
- `🔄 Processing duplicate group: "${duplicateName}" with ${entries.length} occurrences`
1238
- );
1239
-
1240
- // ✅ Leave the first entry unchanged and only process the extras
1241
- const [keepEntry, ...entriesToProcess] = entries;
1242
- console.log(`✅ Keeping original name for ID: ${keepEntry.id}`);
1243
-
1244
- for (const entry of entriesToProcess) {
1245
- const { id, description, combination } = entry;
1246
-
1247
- console.log(`🎭 Processing duplicate ID ${id}: "${duplicateName}"`);
1248
-
1249
- let newNameEN: string | null = null;
1250
- let newNamePT: string | null = null;
1251
- let attempts = 0;
1252
- const maxAttempts = 3;
1253
-
1254
- while (attempts < maxAttempts) {
1255
- attempts++;
1256
- console.log(
1257
- `📡 Attempt ${attempts}/${maxAttempts} - Requesting AI for a new name...`
1258
- );
1259
-
1260
- // ✅ Generate a new name, passing dynamically tracked duplicates
1261
- const messages = this.context
1262
- .buildLLMMessages()
1263
- .regenerateConceptName({
1264
- conceptSlug,
1265
- oldNameEN: duplicateName,
1266
- description,
1267
- recentNames: Array.from(recentDuplicates),
1268
- });
1269
-
1270
- const aiResponse = await this.context
1271
- .api()
1272
- .callTogether.single(messages, {});
1273
-
1274
- if (!aiResponse) {
1275
- console.warn(
1276
- `⚠️ AI failed to generate a new name for ID ${id}, skipping.`
1277
- );
1278
- break;
1279
- }
1280
-
1281
- console.log(`📝 Raw AI Response:\n${aiResponse}`);
1282
-
1283
- // ✅ Mapping for concept names with proper PT articles
1284
- const conceptMapping = {
1285
- amulet: { en: 'Amulet', pt: 'Amuleto', articlePT: 'O' },
1286
- crown: { en: 'Crown', pt: 'Coroa', articlePT: 'A' },
1287
- scepter: { en: 'Scepter', pt: 'Cetro', articlePT: 'O' },
1288
- lantern: { en: 'Lantern', pt: 'Candeia', articlePT: 'A' },
1289
- ring: { en: 'Ring', pt: 'Anel', articlePT: 'O' },
1290
- orb: { en: 'Orb', pt: 'Orbe', articlePT: 'O' },
1291
- };
1292
-
1293
- const {
1294
- en: conceptEN,
1295
- pt: conceptPT,
1296
- articlePT,
1297
- } = conceptMapping[conceptSlug] || {
1298
- en: conceptSlug,
1299
- pt: conceptSlug,
1300
- articlePT: 'O', // Default to "O" if concept is missing
1301
- };
1302
-
1303
- // ✅ Strict regex matching for English and Portuguese
1304
- const enMatch = aiResponse.match(
1305
- new RegExp(`EN:\\s*The ${conceptEN} of (.+)`)
1306
- );
1307
- const ptMatch = aiResponse.match(
1308
- new RegExp(`PT:\\s*${articlePT} ${conceptPT} de (.+)`)
1309
- );
1310
-
1311
- if (!enMatch || !ptMatch) {
1312
- console.error(
1313
- `❌ Invalid AI response format for ID ${id}, retrying...`
1314
- );
1315
- continue;
1316
- }
1317
-
1318
- newNameEN = `The ${conceptEN} of ${enMatch[1].trim()}`;
1319
- newNamePT = `${articlePT} ${conceptPT} de ${ptMatch[1].trim()}`;
1320
-
1321
- console.log(
1322
- `🎨 Extracted Names: EN - "${newNameEN}", PT - "${newNamePT}"`
1323
- );
1324
-
1325
- // ✅ Check if the new name is already in use in D1 before updating
1326
- const existingEntry = await db
1327
- .select({ id: conceptsData.id })
1328
- .from(conceptsData)
1329
- .where(eq(conceptsData.name, newNameEN))
1330
- .execute();
1331
-
1332
- if (existingEntry.length > 0 || recentDuplicates.has(newNameEN)) {
1333
- console.warn(`⚠️ Duplicate detected: "${newNameEN}", retrying...`);
1334
- continue;
1335
- }
1336
-
1337
- recentDuplicates.add(newNameEN);
1338
- break; // ✅ Found a valid new name
1339
- }
1340
-
1341
- if (!newNameEN || !newNamePT) {
1342
- console.error(
1343
- `🚨 Failed to generate a unique name for ID ${id} after ${maxAttempts} attempts.`
1344
- );
1345
- failedEntries++;
1346
- continue;
1347
- }
1348
-
1349
- console.log(
1350
- `✅ Updating names: EN - "${newNameEN}", PT - "${newNamePT}"`
1351
- );
1352
-
1353
- // ✅ Update English name using the ID
1354
- await db
1355
- .update(conceptsData)
1356
- .set({ name: newNameEN })
1357
- .where(
1358
- and(eq(conceptsData.id, id), eq(conceptsData.language, 'en-us'))
1359
- )
1360
- .execute();
1361
-
1362
- // ✅ Convert English ID to Portuguese ID by replacing `:en-us:` with `:pt-br:`
1363
- const ptId = id.replace(':en-us:', ':pt-br:');
1364
-
1365
- // ✅ Update Portuguese name using the derived PT ID
1366
- await db
1367
- .update(conceptsData)
1368
- .set({ name: newNamePT })
1369
- .where(
1370
- and(eq(conceptsData.id, ptId), eq(conceptsData.language, 'pt-br'))
1371
- )
1372
- .execute();
1373
-
1374
- console.log(
1375
- `📝 Updated IDs: EN (${id}) -> "${newNameEN}", PT (${ptId}) -> "${newNamePT}"`
1376
- );
1377
-
1378
- updatedEntries++;
1379
- }
1380
- }
1381
-
1382
- const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
1383
-
1384
- console.log(`🎉 [COMPLETE] Regeneration finished in ${elapsedTime}s.`);
1385
- console.log(`✅ Successfully updated: ${updatedEntries}`);
1386
- console.log(`⚠️ Skipped (already unique): ${skippedEntries}`);
1387
- console.log(`🚨 Failed attempts: ${failedEntries}`);
1388
-
1389
- const remainingDuplicates = await db
1390
- .select({ count: sql<number>`COUNT(*)` }) // ✅ Count only duplicates
1391
- .from(conceptsData)
1392
- .where(
1393
- and(
1394
- eq(conceptsData.conceptSlug, conceptSlug), // ✅ Filter by concept
1395
- sql`name IN (
1396
- SELECT name FROM concepts_data
1397
- WHERE concept_slug = ${conceptSlug}
1398
- GROUP BY name
1399
- HAVING COUNT(*) > 1
1400
- )`
1401
- )
1402
- )
1403
- .execute()
1404
- .then((res) => res[0]?.count || 0);
1405
-
1406
- console.log(
1407
- `🔄 Remaining duplicates for ${conceptSlug}: ${remainingDuplicates}`
1408
- );
1409
-
1410
- return {
1411
- message: `Duplicate names processed and regenerated for ${conceptSlug}.`,
1412
- updated: updatedEntries,
1413
- skipped: skippedEntries,
1414
- failed: failedEntries,
1415
- remainingDuplicates,
1416
- elapsedTime,
1417
- };
1418
- }
1419
-
1420
- async cacheConceptNamesInBatches(conceptSlug: Concept, batchSize = 500) {
1421
- console.log(
1422
- `🔄 Fetching all EN names for '${conceptSlug}' from KV in batches...`
1423
- );
1424
-
1425
- const kvStore = this.context.env.KV_CONCEPTS;
1426
- const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
1427
- let cursor: string | undefined = undefined;
1428
- let nameKeyMap: Record<string, string> = {}; // { name: kvKey }
1429
- let totalKeysScanned = 0;
1430
- let batchIndex = 0;
1431
- const prefix = `concepts:en-us:${conceptSlug}:`; // ✅ Fetch only for this concept
1432
-
1433
- do {
1434
- const response: KVNamespaceListResult<KVConcept> = await kvStore.list({
1435
- prefix,
1436
- cursor,
1437
- limit: batchSize, // ✅ Fetch in batches
1438
- });
1439
-
1440
- const {
1441
- keys,
1442
- cursor: newCursor,
1443
- }: { keys: KVNamespaceListKey<KVConcept>[]; cursor?: string } = response;
1444
- totalKeysScanned += keys.length;
1445
-
1446
- for (const key of keys) {
1447
- const kvData = await kvStore.get<KVConcept>(key.name, 'json');
1448
- if (kvData?.name) {
1449
- nameKeyMap[kvData.name] = key.name; // Map name to its KV key
1450
- }
1451
- }
1452
-
1453
- // ✅ Store this batch separately for the concept to avoid exceeding KV limits
1454
- await kvCacheStore.put(
1455
- `cache:concepts:en-us:${conceptSlug}:${batchIndex}`,
1456
- JSON.stringify(nameKeyMap)
1457
- );
1458
- console.log(
1459
- `📦 Stored batch ${batchIndex} for ${conceptSlug}, ${
1460
- Object.keys(nameKeyMap).length
1461
- } names`
1462
- );
1463
-
1464
- batchIndex++;
1465
- nameKeyMap = {}; // ✅ Clear for the next batch
1466
- cursor = newCursor;
1467
- } while (cursor);
1468
-
1469
- console.log(
1470
- `✅ Cached ${totalKeysScanned} EN names for ${conceptSlug} in ${batchIndex} batches.`
1471
- );
1472
- return {
1473
- message: `Cached ${totalKeysScanned} EN names for ${conceptSlug} in ${batchIndex} batches.`,
1474
- };
1475
- }
1476
-
1477
- async cacheDuplicateConcepts(conceptSlug: Concept) {
1478
- console.log(`🔍 Identifying duplicate entries for concept: ${conceptSlug}`);
1479
-
1480
- const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
1481
- let cursor: string | undefined = undefined;
1482
- let batchIndex = 0;
1483
- let duplicateKeys: string[] = [];
1484
-
1485
- do {
1486
- const batchKey = `cache:concepts:en-us:${conceptSlug}:${batchIndex}`;
1487
- const batchData = await kvCacheStore.get<{ [name: string]: string[] }>(
1488
- batchKey,
1489
- 'json'
1490
- );
1491
-
1492
- if (!batchData) {
1493
- console.log(`⚠️ No data found for batch: ${batchKey}, stopping.`);
1494
- break;
1495
- }
1496
-
1497
- console.log(
1498
- `📦 Processing batch ${batchIndex} with ${
1499
- Object.keys(batchData).length
1500
- } names`
1501
- );
1502
-
1503
- for (const [name, keys] of Object.entries(batchData)) {
1504
- if (keys.length > 1) {
1505
- // Collect duplicate entries (all but the first one)
1506
- duplicateKeys.push(...keys.slice(1));
1507
- }
1508
- }
1509
-
1510
- batchIndex++;
1511
- } while (true);
1512
-
1513
- if (duplicateKeys.length === 0) {
1514
- console.log(`✅ No duplicates found for ${conceptSlug}.`);
1515
- return;
1516
- }
1517
-
1518
- console.log(
1519
- `🚨 Found ${duplicateKeys.length} duplicate entries for ${conceptSlug}`
1520
- );
1521
-
1522
- // Store the duplicate keys list in KV
1523
- await kvCacheStore.put(
1524
- `cache:duplicates:${conceptSlug}`,
1525
- JSON.stringify(duplicateKeys)
1526
- );
1527
-
1528
- console.log(`✅ Stored duplicate keys for ${conceptSlug} in cache.`);
1529
- }
1530
-
1531
1121
  async generateAstroReportContent(
1532
- params: AstroReportParams | { entityType: string; name: string; override?: boolean },
1122
+ params:
1123
+ | AstroReportParams
1124
+ | { entityType: string; name: string; override?: boolean },
1533
1125
  override: boolean = false
1534
1126
  ): Promise<void> {
1535
1127
  console.log(
1536
- `🚀 Generating content for ${JSON.stringify(params)}, override: ${override}`
1128
+ `🚀 Generating content for ${JSON.stringify(
1129
+ params
1130
+ )}, override: ${override}`
1537
1131
  );
1538
-
1132
+
1539
1133
  const db = drizzle(this.context.env.DB);
1540
1134
  let table, whereClause;
1541
-
1135
+
1542
1136
  let report: AstroReportRow | DescriptionTemplateRow;
1543
1137
  const isReport = 'reportType' in params;
1544
-
1138
+
1545
1139
  if (isReport) {
1546
1140
  switch (params.reportType) {
1547
1141
  case 'sign':
@@ -1587,18 +1181,25 @@ export class ConceptService {
1587
1181
  break;
1588
1182
  case 'pure':
1589
1183
  if (!('dominantElement' in params))
1590
- throw new Error('Missing dominantElement for pure element report');
1184
+ throw new Error(
1185
+ 'Missing dominantElement for pure element report'
1186
+ );
1591
1187
  nameValue = params.dominantElement;
1592
1188
  break;
1593
1189
  case 'preponderant_lacking':
1594
- if (!('dominantElement' in params) || !('lackingElement' in params))
1190
+ if (
1191
+ !('dominantElement' in params) ||
1192
+ !('lackingElement' in params)
1193
+ )
1595
1194
  throw new Error(
1596
1195
  'Missing dominantElement or lackingElement for preponderant_lacking element report'
1597
1196
  );
1598
1197
  nameValue = `${params.dominantElement}-${params.lackingElement}`;
1599
1198
  break;
1600
1199
  default:
1601
- throw new Error(`Unknown element subtype: ${(params as any).subtype}`);
1200
+ throw new Error(
1201
+ `Unknown element subtype: ${(params as any).subtype}`
1202
+ );
1602
1203
  }
1603
1204
  } else {
1604
1205
  if (!('name' in params))
@@ -1630,21 +1231,24 @@ export class ConceptService {
1630
1231
  .where(whereClause)
1631
1232
  .get()) as DescriptionTemplateRow;
1632
1233
  }
1633
-
1234
+
1634
1235
  const id = report.id;
1635
1236
  const hasContent = isReport
1636
- ? (report as AstroReportRow).enReport && (report as AstroReportRow).ptReport
1637
- : (report as DescriptionTemplateRow).enDescription !== 'Pending generation' &&
1638
- (report as DescriptionTemplateRow).ptDescription !== 'Pendente de geração';
1639
-
1237
+ ? (report as AstroReportRow).enReport &&
1238
+ (report as AstroReportRow).ptReport
1239
+ : (report as DescriptionTemplateRow).enDescription !==
1240
+ 'Pending generation' &&
1241
+ (report as DescriptionTemplateRow).ptDescription !==
1242
+ 'Pendente de geração';
1243
+
1640
1244
  if (!override && hasContent) {
1641
1245
  console.log(`⚡ Content already exists for ${id}, skipping`);
1642
1246
  return;
1643
1247
  }
1644
-
1248
+
1645
1249
  let attempts = 0;
1646
1250
  const maxAttempts = 2;
1647
-
1251
+
1648
1252
  while (attempts < maxAttempts) {
1649
1253
  let phase = 'generation';
1650
1254
  try {
@@ -1654,7 +1258,7 @@ export class ConceptService {
1654
1258
  isReport ? 'report' : 'description'
1655
1259
  } content for ${id}...`
1656
1260
  );
1657
-
1261
+
1658
1262
  let messages: ChatMessages;
1659
1263
  if (isReport) {
1660
1264
  messages = this.context
@@ -1665,7 +1269,7 @@ export class ConceptService {
1665
1269
  } else {
1666
1270
  messages = this.generateDescriptionMessages(params);
1667
1271
  }
1668
-
1272
+
1669
1273
  console.log(`📨 Sending messages to AI: ${JSON.stringify(messages)}`);
1670
1274
  const response = await this.context
1671
1275
  .api()
@@ -1673,19 +1277,20 @@ export class ConceptService {
1673
1277
  if (!response) {
1674
1278
  throw new Error(`❌ AI returned an empty response for ${id}`);
1675
1279
  }
1676
-
1280
+
1677
1281
  phase = 'parsing';
1678
1282
  console.log(
1679
1283
  `📝 Received AI response for ${id}: ${response.slice(0, 200)}${
1680
1284
  response.length > 200 ? '...' : ''
1681
1285
  }`
1682
1286
  );
1683
-
1287
+
1684
1288
  if (isReport) {
1685
- const { enReport, ptReport } = await this.parseAstrologicalReportContent(
1686
- (params as AstroReportParams).reportType,
1687
- response
1688
- );
1289
+ const { enReport, ptReport } =
1290
+ await this.parseAstrologicalReportContent(
1291
+ (params as AstroReportParams).reportType,
1292
+ response
1293
+ );
1689
1294
  console.log(`💾 Storing report content for ${id}`);
1690
1295
  await db
1691
1296
  .update(table as any)
@@ -1696,7 +1301,8 @@ export class ConceptService {
1696
1301
  .where(eq((table as any).id, id))
1697
1302
  .run();
1698
1303
  } else {
1699
- const { enDescription, ptDescription } = await this.parseDescriptionContent(response);
1304
+ const { enDescription, ptDescription } =
1305
+ await this.parseDescriptionContent(response);
1700
1306
  console.log(`💾 Storing description content for ${id}`);
1701
1307
  await db
1702
1308
  .update(table as any)
@@ -1707,16 +1313,18 @@ export class ConceptService {
1707
1313
  .where(eq(astroDescriptionTemplates.id, id))
1708
1314
  .run();
1709
1315
  }
1710
-
1316
+
1711
1317
  console.log(
1712
1318
  `✅ ${isReport ? 'Report' : 'Description'} content stored for ${id}`
1713
1319
  );
1714
1320
  return;
1715
1321
  } catch (error) {
1716
1322
  console.error(
1717
- `❌ Attempt ${attempts} failed at phase: ${phase} for ${id}: ${(error as Error).message}`
1323
+ `❌ Attempt ${attempts} failed at phase: ${phase} for ${id}: ${
1324
+ (error as Error).message
1325
+ }`
1718
1326
  );
1719
-
1327
+
1720
1328
  const failureKey = `failures:astro:${
1721
1329
  isReport ? (params as AstroReportParams).reportType : 'description'
1722
1330
  }:${id}`;
@@ -1730,7 +1338,7 @@ export class ConceptService {
1730
1338
  timestamp: new Date().toISOString(),
1731
1339
  })
1732
1340
  );
1733
-
1341
+
1734
1342
  if (attempts >= maxAttempts) {
1735
1343
  console.error(
1736
1344
  `🚨 All ${maxAttempts} attempts failed at phase ${phase} for ${id}`
@@ -1741,7 +1349,7 @@ export class ConceptService {
1741
1349
  } for ${id} after ${maxAttempts} attempts`
1742
1350
  );
1743
1351
  }
1744
-
1352
+
1745
1353
  console.log('🔁 Retrying...');
1746
1354
  await new Promise((resolve) => setTimeout(resolve, 2000));
1747
1355
  }
@@ -1862,58 +1470,66 @@ export class ConceptService {
1862
1470
 
1863
1471
  private parseAstrologicalReport(response: string): AstrologicalReport[] {
1864
1472
  console.log('📌 Starting parseAstrologicalReport');
1865
-
1473
+
1866
1474
  // Step 1: Strip any unexpected introductory text
1867
1475
  const prefixRegex = /^[\s\S]*?(?=--\s*(EN|PT)\s*\n)/;
1868
1476
  response = response.replace(prefixRegex, '').trim();
1869
- console.log(`📝 Cleaned response:\n${response.slice(0, 200)}${response.length > 200 ? '...' : ''}`);
1870
-
1477
+ console.log(
1478
+ `📝 Cleaned response:\n${response.slice(0, 200)}${
1479
+ response.length > 200 ? '...' : ''
1480
+ }`
1481
+ );
1482
+
1871
1483
  // Step 2: Split into language sections
1872
1484
  const languageSections = response
1873
1485
  .split(/--\s*(EN|PT)\s*\n/)
1874
1486
  .filter(Boolean);
1875
1487
  console.log(`🔍 Split response into ${languageSections.length} sections`);
1876
-
1488
+
1877
1489
  // Validate: Expect exactly 4 parts (EN, content, PT, content)
1878
1490
  if (languageSections.length !== 4) {
1879
1491
  throw new Error(
1880
- `Invalid response format: Expected exactly 2 sections (EN and PT), got ${languageSections.length / 2}`
1492
+ `Invalid response format: Expected exactly 2 sections (EN and PT), got ${
1493
+ languageSections.length / 2
1494
+ }`
1881
1495
  );
1882
1496
  }
1883
-
1497
+
1884
1498
  const reports: AstrologicalReport[] = [];
1885
-
1499
+
1886
1500
  for (let i = 0; i < languageSections.length; i += 2) {
1887
1501
  const language = languageSections[i].trim();
1888
1502
  const content = languageSections[i + 1]?.trim();
1889
1503
  console.log(`🚀 Processing section ${i / 2 + 1}: Language = ${language}`);
1890
-
1504
+
1891
1505
  if (!content || !language.match(/^(EN|PT)$/)) {
1892
1506
  throw new Error(
1893
- `Invalid section ${i / 2 + 1}: Language must be "EN" or "PT", got "${language}"`
1507
+ `Invalid section ${
1508
+ i / 2 + 1
1509
+ }: Language must be "EN" or "PT", got "${language}"`
1894
1510
  );
1895
1511
  }
1896
-
1512
+
1897
1513
  console.log(
1898
1514
  `📜 Raw content for ${language}:\n${content.slice(0, 500)}${
1899
1515
  content.length > 500 ? '...' : ''
1900
1516
  }`
1901
1517
  );
1902
-
1518
+
1903
1519
  const report: AstrologicalReport = {
1904
1520
  language,
1905
1521
  title: [],
1906
1522
  sections: {},
1907
1523
  };
1908
-
1524
+
1909
1525
  const lines = content.split('\n').map((line) => line.trim());
1910
1526
  console.log(`🔢 Split into ${lines.length} lines`);
1911
-
1527
+
1912
1528
  let currentSection = '';
1913
1529
  for (let j = 0; j < lines.length; j++) {
1914
1530
  const line = lines[j];
1915
1531
  console.log(`📏 Processing line ${j + 1}: "${line}"`);
1916
-
1532
+
1917
1533
  if (line.startsWith('### ') && report.title.length === 0) {
1918
1534
  const titleContent = cleanText(line.replace('### ', ''));
1919
1535
  console.log(`🏷️ Found title: "${titleContent}"`);
@@ -1923,21 +1539,22 @@ export class ConceptService {
1923
1539
  });
1924
1540
  continue;
1925
1541
  }
1926
-
1542
+
1927
1543
  if (line.startsWith('#### ')) {
1928
1544
  currentSection = cleanText(line.replace('#### ', '').trim());
1929
1545
  console.log(`🗂️ New section: "${currentSection}"`);
1930
- report.sections[currentSection] = report.sections[currentSection] || [];
1546
+ report.sections[currentSection] =
1547
+ report.sections[currentSection] || [];
1931
1548
  continue;
1932
1549
  }
1933
-
1550
+
1934
1551
  if (!line || !currentSection) {
1935
1552
  console.log(
1936
1553
  `⏩ Skipping line: ${!line ? 'Empty' : 'No current section'}`
1937
1554
  );
1938
1555
  continue;
1939
1556
  }
1940
-
1557
+
1941
1558
  // Validate language consistency
1942
1559
  const hasPortuguese = /[ãáâéêíóôõúç]/i.test(line);
1943
1560
  const hasEnglish = /[a-zA-Z]/.test(line); // Fixed regex to check for English letters
@@ -1950,16 +1567,16 @@ export class ConceptService {
1950
1567
  `⚠️ Possible invalid content in PT section: No recognizable characters in "${line}"`
1951
1568
  );
1952
1569
  }
1953
-
1570
+
1954
1571
  if (line.match(/^\d+\.\s/) || line.startsWith('- ')) {
1955
1572
  const listItem = line.replace(/^\d+\.\s|- /, '').trim();
1956
1573
  console.log(`🔹 Bullet point detected: "${listItem}"`);
1957
1574
  const boldMatch = listItem.match(/^\*\*(.+?)\*\*[:\s]*(.+)?$/);
1958
-
1575
+
1959
1576
  if (boldMatch) {
1960
1577
  const title = cleanText(boldMatch[1]);
1961
1578
  let content = boldMatch[2] ? cleanText(boldMatch[2]) : '';
1962
-
1579
+
1963
1580
  if (!content && j + 1 < lines.length) {
1964
1581
  const nextLine = lines[j + 1];
1965
1582
  console.log(`🔍 Checking next line for content: "${nextLine}"`);
@@ -1974,7 +1591,7 @@ export class ConceptService {
1974
1591
  j++;
1975
1592
  }
1976
1593
  }
1977
-
1594
+
1978
1595
  console.log(
1979
1596
  `✅ Bold match found - Title: "${title}", Content: "${
1980
1597
  content || 'None'
@@ -1994,14 +1611,14 @@ export class ConceptService {
1994
1611
  }
1995
1612
  continue;
1996
1613
  }
1997
-
1614
+
1998
1615
  console.log(`📝 Paragraph: "${line}"`);
1999
1616
  report.sections[currentSection].push({
2000
1617
  type: 'p',
2001
1618
  content: cleanText(line),
2002
1619
  });
2003
1620
  }
2004
-
1621
+
2005
1622
  console.log(
2006
1623
  `🏁 Finished parsing ${language} section. Result:\n${JSON.stringify(
2007
1624
  report,
@@ -2011,7 +1628,7 @@ export class ConceptService {
2011
1628
  );
2012
1629
  reports.push(report);
2013
1630
  }
2014
-
1631
+
2015
1632
  console.log(`🎉 Parsing complete. Total reports: ${reports.length}`);
2016
1633
  return reports;
2017
1634
  }
@@ -2092,4 +1709,133 @@ export class ConceptService {
2092
1709
  );
2093
1710
  return true; // Everything is ready
2094
1711
  }
1712
+
1713
+ async getConceptBySlug(conceptSlug: Concept) {
1714
+ const db = this.context.drizzle();
1715
+ const [concept] = await db
1716
+ .select({
1717
+ id: schema.concepts.id,
1718
+ planet1: schema.concepts.planet1,
1719
+ planet2: schema.concepts.planet2,
1720
+ planet3: schema.concepts.planet3,
1721
+ })
1722
+ .from(schema.concepts)
1723
+ .where(eq(schema.concepts.slug, conceptSlug))
1724
+ .limit(1)
1725
+ .execute();
1726
+ return concept || null;
1727
+ }
1728
+
1729
+ async getUserSigns(userId: string, planets: string[]) {
1730
+ const db = this.context.drizzle();
1731
+ const userSigns = await db
1732
+ .select({
1733
+ name: schema.astroPlanets.name,
1734
+ sign: schema.astroPlanets.sign,
1735
+ })
1736
+ .from(schema.astroPlanets)
1737
+ .where(
1738
+ and(
1739
+ eq(schema.astroPlanets.userId, userId),
1740
+ inArray(schema.astroPlanets.name, planets)
1741
+ )
1742
+ )
1743
+ .execute();
1744
+ return userSigns;
1745
+ }
1746
+
1747
+ async getOrCreateConceptCombination(
1748
+ conceptId: string,
1749
+ combinationString: string,
1750
+ planets: string[],
1751
+ signs: { name: string; sign: string }[]
1752
+ ) {
1753
+ const db = this.context.drizzle();
1754
+ const [existing] = await db
1755
+ .select({ id: schema.conceptCombinations.id })
1756
+ .from(schema.conceptCombinations)
1757
+ .where(
1758
+ and(
1759
+ eq(schema.conceptCombinations.conceptId, conceptId),
1760
+ eq(schema.conceptCombinations.combinationString, combinationString)
1761
+ )
1762
+ )
1763
+ .limit(1)
1764
+ .execute();
1765
+
1766
+ if (existing) return existing.id;
1767
+
1768
+ const newId = uuidv4();
1769
+ await db
1770
+ .insert(schema.conceptCombinations)
1771
+ .values({
1772
+ id: newId,
1773
+ conceptId,
1774
+ planet1Sign: signs.find((p) => p.name === planets[0])!.sign,
1775
+ planet2Sign: signs.find((p) => p.name === planets[1])!.sign,
1776
+ planet3Sign: signs.find((p) => p.name === planets[2])!.sign,
1777
+ combinationString,
1778
+ })
1779
+ .execute();
1780
+ return newId;
1781
+ }
1782
+
1783
+ async checkHasPrompt(
1784
+ conceptSlug: Concept,
1785
+ combinationString: string,
1786
+ lang: string = 'en-us'
1787
+ ): Promise<boolean> {
1788
+ const db = this.context.drizzle();
1789
+ const [conceptData] = await db
1790
+ .select({
1791
+ leonardoPrompt: schema.conceptsData.leonardoPrompt,
1792
+ })
1793
+ .from(schema.conceptsData)
1794
+ .where(
1795
+ and(
1796
+ eq(schema.conceptsData.conceptSlug, conceptSlug),
1797
+ eq(schema.conceptsData.combination, combinationString),
1798
+ eq(schema.conceptsData.language, lang)
1799
+ )
1800
+ )
1801
+ .limit(1)
1802
+ .execute();
1803
+
1804
+ return !!conceptData?.leonardoPrompt && conceptData.leonardoPrompt.length > 10;
1805
+ }
1806
+
1807
+ // src/services/ConceptService.ts
1808
+ async checkConceptCompletion(
1809
+ conceptSlug: Concept,
1810
+ combinationString: string,
1811
+ lang: string = 'en-us'
1812
+ ): Promise<boolean> {
1813
+ const db = this.context.drizzle();
1814
+ const [conceptData] = await db
1815
+ .select({
1816
+ leonardoPrompt: schema.conceptsData.leonardoPrompt,
1817
+ postImages: schema.conceptsData.postImages,
1818
+ reelImages: schema.conceptsData.reelImages,
1819
+ })
1820
+ .from(schema.conceptsData)
1821
+ .where(
1822
+ and(
1823
+ eq(schema.conceptsData.conceptSlug, conceptSlug),
1824
+ eq(schema.conceptsData.combination, combinationString),
1825
+ eq(schema.conceptsData.language, lang)
1826
+ )
1827
+ )
1828
+ .limit(1)
1829
+ .execute();
1830
+
1831
+ if (!conceptData) return false; // No concept data found
1832
+
1833
+ // Check if postImages and reelImages are valid non-empty JSON arrays
1834
+ const hasPostImages =
1835
+ conceptData.postImages &&
1836
+ conceptData.postImages.trim().length > 4 && // Ensures length > 4 to exclude "[]"
1837
+ JSON.parse(conceptData.postImages).length > 2;
1838
+
1839
+ return !!conceptData.leonardoPrompt && !!hasPostImages;
1840
+ }
2095
1841
  }
@@ -1,9 +1,7 @@
1
1
  import { inject, injectable } from 'inversify';
2
- import pMap from 'p-map';
3
- import { zodiacSignCombinations } from '../../data/zodiacSignCombinations';
4
- import { Concept, ConceptPhase, ConceptProgress } from '../../types';
2
+ import { Concept } from '../../types';
5
3
  import { AppContext } from '../base';
6
- import { ConceptService } from '../services/ConceptService';
4
+ import { ConceptService } from '../services';
7
5
 
8
6
  @injectable()
9
7
  export class ConceptWorkflow {
@@ -12,173 +10,61 @@ export class ConceptWorkflow {
12
10
  @inject(ConceptService) private conceptService: ConceptService
13
11
  ) {}
14
12
 
15
- async processSingle(
13
+ async execute(
14
+ userId: string,
16
15
  conceptSlug: Concept,
17
16
  combinationString: string,
18
- phase: ConceptPhase,
19
- override?: boolean
17
+ lang: string = 'en-us'
20
18
  ) {
21
19
  console.log(
22
- `🚀 Processing single concept: ${conceptSlug} | ${combinationString} | Phase: ${phase}`
20
+ `[${new Date().toISOString()}] Starting ConceptWorkflow for ${conceptSlug}:${combinationString}`,
21
+ { userId, lang }
23
22
  );
24
23
 
25
24
  try {
26
- switch (phase) {
27
- case 'basic-info':
28
- await this.conceptService.generateBasicInfo(
29
- conceptSlug,
30
- combinationString,
31
- override
32
- );
33
- break;
34
- case 'content':
35
- await this.conceptService.generateContent(
36
- conceptSlug,
37
- combinationString,
38
- override
39
- );
40
- break;
41
- case 'leonardo-prompt':
42
- await this.conceptService.generatePrompt(
43
- conceptSlug,
44
- combinationString
45
- );
46
- break;
47
- case 'images':
48
- await this.conceptService.generateImages(
49
- 'en-us',
50
- conceptSlug,
51
- combinationString
52
- );
53
- break;
54
- default:
55
- throw new Error(`Unsupported phase: ${phase}`);
25
+ // Step 1: Generate Leonardo prompt if missing
26
+ const hasPrompt = await this.conceptService.checkHasPrompt(
27
+ conceptSlug,
28
+ combinationString,
29
+ lang
30
+ );
31
+ if (!hasPrompt) {
32
+ console.log(
33
+ `[${new Date().toISOString()}] Generating prompt for ${conceptSlug}:${combinationString}`
34
+ );
35
+ await this.conceptService.generatePrompt(
36
+ conceptSlug,
37
+ combinationString,
38
+ false
39
+ );
40
+ }
41
+
42
+ // Step 2: Generate images if missing
43
+ if (
44
+ !(await this.conceptService.checkConceptCompletion(
45
+ conceptSlug,
46
+ combinationString,
47
+ lang
48
+ ))
49
+ ) {
50
+ console.log(
51
+ `[${new Date().toISOString()}] Generating images for ${conceptSlug}:${combinationString}`
52
+ );
53
+ await this.conceptService.generateImages(
54
+ conceptSlug,
55
+ combinationString
56
+ );
56
57
  }
57
58
 
58
59
  console.log(
59
- `✅ Concept ${conceptSlug}:${combinationString} successfully processed for phase: ${phase}`
60
+ `[${new Date().toISOString()}] Concept generation completed for ${conceptSlug}:${combinationString}`
60
61
  );
61
62
  } catch (error) {
62
63
  console.error(
63
- `❌ Error processing concept ${conceptSlug}:${combinationString} | Phase: ${phase}:`,
64
+ `[${new Date().toISOString()}] Error in ConceptWorkflow for ${conceptSlug}:${combinationString}:`,
64
65
  error
65
66
  );
67
+ throw error; // Let the queue handle retries
66
68
  }
67
69
  }
68
-
69
- async processDuplicateNames(conceptSlug: Concept) {
70
- console.log(`🔍 Initiating duplicate name resolution for ${conceptSlug}`);
71
-
72
- try {
73
- await this.conceptService.regenerateDuplicateNamesForConcept(conceptSlug);
74
- console.log(`✅ Duplicate names resolved for ${conceptSlug}`);
75
- } catch (error) {
76
- console.error(`❌ Error resolving duplicates for ${conceptSlug}:`, error);
77
- }
78
- }
79
-
80
- async processBatch(
81
- conceptSlug: Concept,
82
- combinations: string[],
83
- phase: ConceptPhase,
84
- override: boolean = false
85
- ) {
86
- console.log(
87
- `🚀 Processing batch for concept: ${conceptSlug}, Phase: ${phase}, Total Combinations: ${combinations.length}`
88
- );
89
-
90
- const concurrency = 5;
91
- const failedCombinations: string[] = [];
92
-
93
- // ✅ Get Durable Object stub for progress tracking
94
- const id = this.context.env.CONCEPT_NAMES_DO.idFromName(conceptSlug);
95
- const stub = this.context.env.CONCEPT_NAMES_DO.get(id);
96
-
97
- // ✅ Try to load last checkpoint from DO
98
- let lastIndex = 0;
99
- try {
100
- const progressRes = await stub.fetch(`https://internal/progress`);
101
- if (progressRes.ok) {
102
- const progressData = (await progressRes.json()) as ConceptProgress;
103
- lastIndex = progressData.lastIndex || 0;
104
- console.log(`🔄 Resuming from index ${lastIndex}`);
105
- } else {
106
- console.log(`📌 No previous checkpoint found, starting from scratch.`);
107
- }
108
- } catch (error) {
109
- console.error(`❌ Error fetching progress from DO:`, error);
110
- }
111
-
112
- // ✅ Filter unprocessed combinations
113
- const remainingCombinations = combinations.slice(lastIndex);
114
- console.log(
115
- `⏳ Processing ${remainingCombinations.length} remaining combinations...`
116
- );
117
-
118
- await pMap(
119
- remainingCombinations,
120
- async (combination, index) => {
121
- const currentIndex = lastIndex + index;
122
- try {
123
- console.log(
124
- `🛠️ Processing combination ${currentIndex}/${combinations.length}: ${combination}`
125
- );
126
- await this.processSingle(conceptSlug, combination, phase, override);
127
-
128
- // ✅ Save progress every 10 iterations in DO
129
- if (currentIndex % 10 === 0) {
130
- await stub.fetch(`https://internal/set-progress`, {
131
- method: 'POST',
132
- body: JSON.stringify({ lastIndex: currentIndex }),
133
- headers: { 'Content-Type': 'application/json' },
134
- });
135
- console.log(`✅ Checkpoint saved at index ${currentIndex}`);
136
- }
137
- } catch (error) {
138
- console.error(`❌ Error processing ${combination}:`, error);
139
- failedCombinations.push(combination);
140
- }
141
- },
142
- { concurrency }
143
- );
144
-
145
- console.log(
146
- `✅ Batch processing completed for ${conceptSlug}, Phase: ${phase}`
147
- );
148
-
149
- // ✅ Retry failed combinations after all batches
150
- if (failedCombinations.length > 0) {
151
- console.warn(`⚠️ Retrying failed combinations:`, failedCombinations);
152
- await this.processBatch(conceptSlug, failedCombinations, phase, override);
153
- }
154
-
155
- console.log(
156
- `🎉 All combinations successfully processed for ${conceptSlug}!`
157
- );
158
- }
159
-
160
- async processAllCombinations(
161
- conceptSlug: Concept,
162
- phase: ConceptPhase,
163
- override?: boolean
164
- ) {
165
- console.log(`🔍 Fetching all possible combinations for: ${conceptSlug}`);
166
-
167
- // 🔥 Replace with the actual function that retrieves all pre-defined combinations from KV or DB
168
- const combinations = zodiacSignCombinations;
169
- if (!combinations.length) {
170
- console.warn(`⚠️ No combinations found for ${conceptSlug}, skipping.`);
171
- return;
172
- }
173
-
174
- console.log(
175
- `🚀 Processing ALL combinations for ${conceptSlug}, Phase: ${phase}, Total: ${combinations.length}`
176
- );
177
-
178
- await this.processBatch(conceptSlug, combinations, phase, override);
179
-
180
- console.log(
181
- `✅ All combinations processed for concept: ${conceptSlug}, Phase: ${phase}`
182
- );
183
- }
184
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zodic/shared",
3
- "version": "0.0.368",
3
+ "version": "0.0.370",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {