@zodic/shared 0.0.224 → 0.0.226
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 +125 -63
- package/package.json +1 -1
|
@@ -1034,14 +1034,16 @@ export class ConceptService {
|
|
|
1034
1034
|
conceptSlug: Concept,
|
|
1035
1035
|
maxEntries?: number
|
|
1036
1036
|
) {
|
|
1037
|
-
console.log(
|
|
1037
|
+
console.log(
|
|
1038
|
+
`🔄 [START] Regenerating duplicate names for concept: ${conceptSlug}...`
|
|
1039
|
+
);
|
|
1038
1040
|
const startTime = Date.now();
|
|
1039
|
-
|
|
1041
|
+
|
|
1040
1042
|
const db = drizzle(this.context.env.DB);
|
|
1041
|
-
|
|
1043
|
+
|
|
1042
1044
|
// ✅ Step 1: Fetch duplicate entries filtered by conceptSlug
|
|
1043
1045
|
console.log(`📡 Fetching duplicate entries for concept: ${conceptSlug}...`);
|
|
1044
|
-
|
|
1046
|
+
|
|
1045
1047
|
const duplicateEntries = await db
|
|
1046
1048
|
.select({
|
|
1047
1049
|
id: conceptsData.id,
|
|
@@ -1052,25 +1054,35 @@ export class ConceptService {
|
|
|
1052
1054
|
.from(conceptsData)
|
|
1053
1055
|
.where(
|
|
1054
1056
|
and(
|
|
1055
|
-
eq(conceptsData.language, 'en-us'), // English
|
|
1057
|
+
eq(conceptsData.language, 'en-us'), // ✅ English only
|
|
1056
1058
|
eq(conceptsData.conceptSlug, conceptSlug), // ✅ Filter by concept
|
|
1057
|
-
sql`
|
|
1058
|
-
|
|
1059
|
+
sql`
|
|
1060
|
+
name IN (
|
|
1061
|
+
SELECT name
|
|
1062
|
+
FROM concepts_data
|
|
1059
1063
|
WHERE language = 'en-us' AND concept_slug = ${conceptSlug}
|
|
1060
|
-
GROUP BY name
|
|
1061
|
-
|
|
1064
|
+
GROUP BY name
|
|
1065
|
+
HAVING COUNT(*) > 1
|
|
1066
|
+
)
|
|
1067
|
+
`
|
|
1062
1068
|
)
|
|
1063
1069
|
)
|
|
1070
|
+
.orderBy(conceptsData.name) // ✅ Order by name for better grouping
|
|
1064
1071
|
.limit(maxEntries || 50) // ✅ Apply optional limit
|
|
1065
1072
|
.execute();
|
|
1066
|
-
|
|
1073
|
+
|
|
1067
1074
|
if (!duplicateEntries.length) {
|
|
1068
1075
|
console.log(`🎉 No duplicates found for ${conceptSlug}.`);
|
|
1069
|
-
return {
|
|
1076
|
+
return {
|
|
1077
|
+
message: `No duplicates detected for ${conceptSlug}.`,
|
|
1078
|
+
remainingDuplicates: 0,
|
|
1079
|
+
};
|
|
1070
1080
|
}
|
|
1071
|
-
|
|
1072
|
-
console.log(
|
|
1073
|
-
|
|
1081
|
+
|
|
1082
|
+
console.log(
|
|
1083
|
+
`🔍 Found ${duplicateEntries.length} duplicate entries to process.`
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1074
1086
|
// ✅ Group duplicates by name
|
|
1075
1087
|
const duplicateGroups = new Map<string, typeof duplicateEntries>();
|
|
1076
1088
|
for (const entry of duplicateEntries) {
|
|
@@ -1079,50 +1091,60 @@ export class ConceptService {
|
|
|
1079
1091
|
}
|
|
1080
1092
|
duplicateGroups.get(entry.name)!.push(entry);
|
|
1081
1093
|
}
|
|
1082
|
-
|
|
1094
|
+
|
|
1083
1095
|
let recentDuplicates = new Set<string>(); // ✅ Track newly generated names
|
|
1084
1096
|
let updatedEntries = 0;
|
|
1085
1097
|
let skippedEntries = 0;
|
|
1086
1098
|
let failedEntries = 0;
|
|
1087
|
-
|
|
1099
|
+
|
|
1088
1100
|
for (const [duplicateName, entries] of duplicateGroups.entries()) {
|
|
1089
|
-
console.log(
|
|
1090
|
-
|
|
1101
|
+
console.log(
|
|
1102
|
+
`🔄 Processing duplicate group: "${duplicateName}" with ${entries.length} occurrences`
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1091
1105
|
// ✅ Leave the first entry unchanged and only process the extras
|
|
1092
1106
|
const [keepEntry, ...entriesToProcess] = entries;
|
|
1093
1107
|
console.log(`✅ Keeping original name for ID: ${keepEntry.id}`);
|
|
1094
|
-
|
|
1108
|
+
|
|
1095
1109
|
for (const entry of entriesToProcess) {
|
|
1096
1110
|
const { id, description, combination } = entry;
|
|
1097
|
-
|
|
1111
|
+
|
|
1098
1112
|
console.log(`🎭 Processing duplicate ID ${id}: "${duplicateName}"`);
|
|
1099
|
-
|
|
1113
|
+
|
|
1100
1114
|
let newNameEN: string | null = null;
|
|
1101
1115
|
let newNamePT: string | null = null;
|
|
1102
1116
|
let attempts = 0;
|
|
1103
1117
|
const maxAttempts = 3;
|
|
1104
|
-
|
|
1118
|
+
|
|
1105
1119
|
while (attempts < maxAttempts) {
|
|
1106
1120
|
attempts++;
|
|
1107
|
-
console.log(
|
|
1108
|
-
|
|
1121
|
+
console.log(
|
|
1122
|
+
`📡 Attempt ${attempts}/${maxAttempts} - Requesting AI for a new name...`
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1109
1125
|
// ✅ Generate a new name, passing dynamically tracked duplicates
|
|
1110
|
-
const messages = this.context
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1126
|
+
const messages = this.context
|
|
1127
|
+
.buildLLMMessages()
|
|
1128
|
+
.regenerateConceptName({
|
|
1129
|
+
conceptSlug,
|
|
1130
|
+
oldNameEN: duplicateName,
|
|
1131
|
+
description,
|
|
1132
|
+
recentNames: Array.from(recentDuplicates),
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
const aiResponse = await this.context
|
|
1136
|
+
.api()
|
|
1137
|
+
.callTogether.single(messages, {});
|
|
1138
|
+
|
|
1119
1139
|
if (!aiResponse) {
|
|
1120
|
-
console.warn(
|
|
1140
|
+
console.warn(
|
|
1141
|
+
`⚠️ AI failed to generate a new name for ID ${id}, skipping.`
|
|
1142
|
+
);
|
|
1121
1143
|
break;
|
|
1122
1144
|
}
|
|
1123
|
-
|
|
1145
|
+
|
|
1124
1146
|
console.log(`📝 Raw AI Response:\n${aiResponse}`);
|
|
1125
|
-
|
|
1147
|
+
|
|
1126
1148
|
// ✅ Mapping for concept names with proper PT articles
|
|
1127
1149
|
const conceptMapping = {
|
|
1128
1150
|
amulet: { en: 'Amulet', pt: 'Amuleto', articlePT: 'O' },
|
|
@@ -1132,7 +1154,7 @@ export class ConceptService {
|
|
|
1132
1154
|
ring: { en: 'Ring', pt: 'Anel', articlePT: 'O' },
|
|
1133
1155
|
orb: { en: 'Orb', pt: 'Orbe', articlePT: 'O' },
|
|
1134
1156
|
};
|
|
1135
|
-
|
|
1157
|
+
|
|
1136
1158
|
const {
|
|
1137
1159
|
en: conceptEN,
|
|
1138
1160
|
pt: conceptPT,
|
|
@@ -1142,80 +1164,120 @@ export class ConceptService {
|
|
|
1142
1164
|
pt: conceptSlug,
|
|
1143
1165
|
articlePT: 'O', // Default to "O" if concept is missing
|
|
1144
1166
|
};
|
|
1145
|
-
|
|
1167
|
+
|
|
1146
1168
|
// ✅ Strict regex matching for English and Portuguese
|
|
1147
|
-
const enMatch = aiResponse.match(
|
|
1148
|
-
|
|
1149
|
-
|
|
1169
|
+
const enMatch = aiResponse.match(
|
|
1170
|
+
new RegExp(`EN:\\s*The ${conceptEN} of (.+)`)
|
|
1171
|
+
);
|
|
1172
|
+
const ptMatch = aiResponse.match(
|
|
1173
|
+
new RegExp(`PT:\\s*${articlePT} ${conceptPT} de (.+)`)
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1150
1176
|
if (!enMatch || !ptMatch) {
|
|
1151
|
-
console.error(
|
|
1177
|
+
console.error(
|
|
1178
|
+
`❌ Invalid AI response format for ID ${id}, retrying...`
|
|
1179
|
+
);
|
|
1152
1180
|
continue;
|
|
1153
1181
|
}
|
|
1154
|
-
|
|
1182
|
+
|
|
1155
1183
|
newNameEN = `The ${conceptEN} of ${enMatch[1].trim()}`;
|
|
1156
1184
|
newNamePT = `${articlePT} ${conceptPT} de ${ptMatch[1].trim()}`;
|
|
1157
|
-
|
|
1158
|
-
console.log(
|
|
1159
|
-
|
|
1185
|
+
|
|
1186
|
+
console.log(
|
|
1187
|
+
`🎨 Extracted Names: EN - "${newNameEN}", PT - "${newNamePT}"`
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1160
1190
|
// ✅ Check if the new name is already in use in D1 before updating
|
|
1161
1191
|
const existingEntry = await db
|
|
1162
1192
|
.select({ id: conceptsData.id })
|
|
1163
1193
|
.from(conceptsData)
|
|
1164
1194
|
.where(eq(conceptsData.name, newNameEN))
|
|
1165
1195
|
.execute();
|
|
1166
|
-
|
|
1196
|
+
|
|
1167
1197
|
if (existingEntry.length > 0 || recentDuplicates.has(newNameEN)) {
|
|
1168
1198
|
console.warn(`⚠️ Duplicate detected: "${newNameEN}", retrying...`);
|
|
1169
1199
|
continue;
|
|
1170
1200
|
}
|
|
1171
|
-
|
|
1201
|
+
|
|
1172
1202
|
recentDuplicates.add(newNameEN);
|
|
1173
1203
|
break; // ✅ Found a valid new name
|
|
1174
1204
|
}
|
|
1175
|
-
|
|
1205
|
+
|
|
1176
1206
|
if (!newNameEN || !newNamePT) {
|
|
1177
|
-
console.error(
|
|
1207
|
+
console.error(
|
|
1208
|
+
`🚨 Failed to generate a unique name for ID ${id} after ${maxAttempts} attempts.`
|
|
1209
|
+
);
|
|
1178
1210
|
failedEntries++;
|
|
1179
1211
|
continue;
|
|
1180
1212
|
}
|
|
1181
|
-
|
|
1182
|
-
console.log(
|
|
1183
|
-
|
|
1213
|
+
|
|
1214
|
+
console.log(
|
|
1215
|
+
`✅ Updating names: EN - "${newNameEN}", PT - "${newNamePT}"`
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1184
1218
|
// ✅ Update English name using the ID
|
|
1185
1219
|
await db
|
|
1186
1220
|
.update(conceptsData)
|
|
1187
1221
|
.set({ name: newNameEN })
|
|
1188
|
-
.where(
|
|
1222
|
+
.where(
|
|
1223
|
+
and(eq(conceptsData.id, id), eq(conceptsData.language, 'en-us'))
|
|
1224
|
+
)
|
|
1189
1225
|
.execute();
|
|
1190
|
-
|
|
1226
|
+
|
|
1191
1227
|
// ✅ Convert English ID to Portuguese ID by replacing `:en-us:` with `:pt-br:`
|
|
1192
1228
|
const ptId = id.replace(':en-us:', ':pt-br:');
|
|
1193
|
-
|
|
1229
|
+
|
|
1194
1230
|
// ✅ Update Portuguese name using the derived PT ID
|
|
1195
1231
|
await db
|
|
1196
1232
|
.update(conceptsData)
|
|
1197
1233
|
.set({ name: newNamePT })
|
|
1198
|
-
.where(
|
|
1234
|
+
.where(
|
|
1235
|
+
and(eq(conceptsData.id, ptId), eq(conceptsData.language, 'pt-br'))
|
|
1236
|
+
)
|
|
1199
1237
|
.execute();
|
|
1200
|
-
|
|
1201
|
-
console.log(
|
|
1202
|
-
|
|
1238
|
+
|
|
1239
|
+
console.log(
|
|
1240
|
+
`📝 Updated IDs: EN (${id}) -> "${newNameEN}", PT (${ptId}) -> "${newNamePT}"`
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1203
1243
|
updatedEntries++;
|
|
1204
1244
|
}
|
|
1205
1245
|
}
|
|
1206
|
-
|
|
1246
|
+
|
|
1207
1247
|
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
1208
|
-
|
|
1248
|
+
|
|
1209
1249
|
console.log(`🎉 [COMPLETE] Regeneration finished in ${elapsedTime}s.`);
|
|
1210
1250
|
console.log(`✅ Successfully updated: ${updatedEntries}`);
|
|
1211
1251
|
console.log(`⚠️ Skipped (already unique): ${skippedEntries}`);
|
|
1212
1252
|
console.log(`🚨 Failed attempts: ${failedEntries}`);
|
|
1213
|
-
|
|
1253
|
+
|
|
1254
|
+
const remainingDuplicates = await db
|
|
1255
|
+
.select({ count: sql<number>`COUNT(*)` }) // ✅ Count only duplicates
|
|
1256
|
+
.from(conceptsData)
|
|
1257
|
+
.where(
|
|
1258
|
+
and(
|
|
1259
|
+
eq(conceptsData.conceptSlug, conceptSlug), // ✅ Filter by concept
|
|
1260
|
+
sql`name IN (
|
|
1261
|
+
SELECT name FROM concepts_data
|
|
1262
|
+
WHERE concept_slug = ${conceptSlug}
|
|
1263
|
+
GROUP BY name
|
|
1264
|
+
HAVING COUNT(*) > 1
|
|
1265
|
+
)`
|
|
1266
|
+
)
|
|
1267
|
+
)
|
|
1268
|
+
.execute()
|
|
1269
|
+
.then((res) => res[0]?.count || 0);
|
|
1270
|
+
|
|
1271
|
+
console.log(
|
|
1272
|
+
`🔄 Remaining duplicates for ${conceptSlug}: ${remainingDuplicates}`
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1214
1275
|
return {
|
|
1215
1276
|
message: `Duplicate names processed and regenerated for ${conceptSlug}.`,
|
|
1216
1277
|
updated: updatedEntries,
|
|
1217
1278
|
skipped: skippedEntries,
|
|
1218
1279
|
failed: failedEntries,
|
|
1280
|
+
remainingDuplicates,
|
|
1219
1281
|
elapsedTime,
|
|
1220
1282
|
};
|
|
1221
1283
|
}
|