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