@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.
- package/app/services/ConceptService.ts +211 -465
- package/app/workflow/ConceptWorkflow.ts +42 -156
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
KVNamespaceListKey,
|
|
3
3
|
KVNamespaceListResult,
|
|
4
4
|
} from '@cloudflare/workers-types';
|
|
5
|
-
import { and, eq, inArray, isNull
|
|
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:
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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 &&
|
|
1637
|
-
|
|
1638
|
-
|
|
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 } =
|
|
1686
|
-
|
|
1687
|
-
|
|
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 } =
|
|
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}: ${
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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] =
|
|
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
|
|
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
|
|
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
|
|
13
|
+
async execute(
|
|
14
|
+
userId: string,
|
|
16
15
|
conceptSlug: Concept,
|
|
17
16
|
combinationString: string,
|
|
18
|
-
|
|
19
|
-
override?: boolean
|
|
17
|
+
lang: string = 'en-us'
|
|
20
18
|
) {
|
|
21
19
|
console.log(
|
|
22
|
-
|
|
20
|
+
`[${new Date().toISOString()}] Starting ConceptWorkflow for ${conceptSlug}:${combinationString}`,
|
|
21
|
+
{ userId, lang }
|
|
23
22
|
);
|
|
24
23
|
|
|
25
24
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
+
`[${new Date().toISOString()}] Concept generation completed for ${conceptSlug}:${combinationString}`
|
|
60
61
|
);
|
|
61
62
|
} catch (error) {
|
|
62
63
|
console.error(
|
|
63
|
-
|
|
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
|
}
|