@zodic/shared 0.0.211 β 0.0.212
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 +419 -368
- package/app/workflow/ConceptWorkflow.ts +11 -0
- package/package.json +1 -1
- package/utils/buildMessages.ts +38 -24
|
@@ -29,67 +29,80 @@ export class ConceptService {
|
|
|
29
29
|
combinationString: string,
|
|
30
30
|
override: boolean = false
|
|
31
31
|
): Promise<void> {
|
|
32
|
-
console.log(
|
|
33
|
-
|
|
32
|
+
console.log(
|
|
33
|
+
`π Generating basic info for concept: ${conceptSlug}, combination: ${combinationString}, override: ${override}`
|
|
34
|
+
);
|
|
35
|
+
|
|
34
36
|
const kvStore = this.context.kvConceptsStore();
|
|
35
37
|
const kvFailuresStore = this.context.kvConceptFailuresStore();
|
|
36
38
|
const kvKeyEN = buildConceptKVKey('en-us', conceptSlug, combinationString);
|
|
37
39
|
const kvKeyPT = buildConceptKVKey('pt-br', conceptSlug, combinationString);
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
// β
Use Durable Object stub
|
|
40
42
|
const id = this.context.env.CONCEPT_NAMES_DO.idFromName(conceptSlug);
|
|
41
43
|
const stub = this.context.env.CONCEPT_NAMES_DO.get(id);
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
console.log(`π‘ Fetching existing KV data for ${conceptSlug}...`);
|
|
44
46
|
const existingEN = await this.getKVConcept(kvKeyEN);
|
|
45
47
|
const existingPT = await this.getKVConcept(kvKeyPT);
|
|
46
|
-
|
|
48
|
+
|
|
47
49
|
if (!override && existingEN.name && existingPT.name) {
|
|
48
50
|
console.log(`β‘ Basic info already exists for ${conceptSlug}, skipping.`);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
|
-
|
|
53
|
+
|
|
52
54
|
let attempts = 0;
|
|
53
55
|
const maxAttempts = 3;
|
|
54
56
|
const MAX_NAME_LENGTH = 44;
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
while (attempts < maxAttempts) {
|
|
57
59
|
let phase = 'generation';
|
|
58
60
|
try {
|
|
59
61
|
attempts++;
|
|
60
|
-
console.log(
|
|
61
|
-
|
|
62
|
+
console.log(
|
|
63
|
+
`π Attempt ${attempts} to generate basic info for ${conceptSlug}...`
|
|
64
|
+
);
|
|
65
|
+
|
|
62
66
|
let allNamesEN: string[] = [];
|
|
63
67
|
let allNamesPT: string[] = [];
|
|
64
68
|
const response = await stub.fetch(`https://internal/names`);
|
|
65
|
-
|
|
69
|
+
|
|
66
70
|
if (response.ok) {
|
|
67
|
-
const data = (await response.json()) as {
|
|
71
|
+
const data = (await response.json()) as {
|
|
72
|
+
'en-us': string[];
|
|
73
|
+
'pt-br': string[];
|
|
74
|
+
};
|
|
68
75
|
allNamesEN = data['en-us'] || [];
|
|
69
76
|
allNamesPT = data['pt-br'] || [];
|
|
70
77
|
}
|
|
71
|
-
|
|
78
|
+
|
|
72
79
|
console.log(`βοΈ Generating new name...`);
|
|
73
|
-
const messages = this.context
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
const messages = this.context
|
|
81
|
+
.buildLLMMessages()
|
|
82
|
+
.generateConceptBasicInfo({
|
|
83
|
+
combination: combinationString,
|
|
84
|
+
conceptSlug,
|
|
85
|
+
existingNames:
|
|
86
|
+
allNamesEN.length > 100 ? allNamesEN.slice(-100) : allNamesEN,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let aiResponse = await this.context
|
|
90
|
+
.api()
|
|
91
|
+
.callTogether.single(messages, {});
|
|
80
92
|
if (!aiResponse) throw new Error(`β AI returned an empty response`);
|
|
81
|
-
|
|
93
|
+
|
|
82
94
|
phase = 'cleaning';
|
|
83
95
|
aiResponse = this.cleanAIResponse(aiResponse);
|
|
84
96
|
|
|
85
|
-
console.log(
|
|
86
|
-
console.log(
|
|
87
|
-
|
|
97
|
+
console.log('!-- aiResponse -> ', aiResponse);
|
|
98
|
+
console.log('!-- aiResponse length -> ', aiResponse.length);
|
|
99
|
+
|
|
88
100
|
phase = 'parsing';
|
|
89
|
-
let { nameEN, descriptionEN, poemEN, namePT, descriptionPT, poemPT } =
|
|
90
|
-
|
|
101
|
+
let { nameEN, descriptionEN, poemEN, namePT, descriptionPT, poemPT } =
|
|
102
|
+
this.parseBasicInfoResponse(aiResponse, conceptSlug);
|
|
103
|
+
|
|
91
104
|
console.log(`π Generated names: EN - "${nameEN}", PT - "${namePT}"`);
|
|
92
|
-
|
|
105
|
+
|
|
93
106
|
// β
Forcefully trim the name to exactly 5 words if it's too long
|
|
94
107
|
const enforceWordLimit = (name: string): string => {
|
|
95
108
|
const words = name.split(/\s+/);
|
|
@@ -98,40 +111,66 @@ export class ConceptService {
|
|
|
98
111
|
}
|
|
99
112
|
return name;
|
|
100
113
|
};
|
|
101
|
-
|
|
114
|
+
|
|
102
115
|
nameEN = enforceWordLimit(nameEN);
|
|
103
116
|
namePT = enforceWordLimit(namePT);
|
|
104
|
-
|
|
117
|
+
|
|
105
118
|
console.log(`βοΈ Trimmed names: EN - "${nameEN}", PT - "${namePT}"`);
|
|
106
|
-
|
|
119
|
+
|
|
107
120
|
// β
Check uniqueness before storing
|
|
108
121
|
if (allNamesEN.includes(nameEN) || allNamesPT.includes(namePT)) {
|
|
109
|
-
console.warn(
|
|
122
|
+
console.warn(
|
|
123
|
+
`β οΈ Duplicate Name Detected: "${nameEN}" or "${namePT}"`
|
|
124
|
+
);
|
|
110
125
|
if (attempts >= maxAttempts) {
|
|
111
|
-
console.log(
|
|
126
|
+
console.log(
|
|
127
|
+
`π¨ Max attempts reached. Storing name despite duplicate.`
|
|
128
|
+
);
|
|
112
129
|
await kvFailuresStore.put(
|
|
113
130
|
`failures:duplicates:${conceptSlug}:${combinationString}`,
|
|
114
|
-
JSON.stringify({
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
nameEN,
|
|
133
|
+
namePT,
|
|
134
|
+
attempts,
|
|
135
|
+
conceptSlug,
|
|
136
|
+
combinationString,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
})
|
|
115
139
|
);
|
|
116
140
|
break;
|
|
117
141
|
}
|
|
118
142
|
continue;
|
|
119
143
|
}
|
|
120
|
-
|
|
144
|
+
|
|
121
145
|
console.log(`π Storing names in Durable Object...`);
|
|
122
146
|
// await stub.fetch(`https://internal/add-name`, { method: 'POST', body: JSON.stringify({ language: 'en-us', name: nameEN }), headers: { 'Content-Type': 'application/json' } });
|
|
123
147
|
// await stub.fetch(`https://internal/add-name`, { method: 'POST', body: JSON.stringify({ language: 'pt-br', name: namePT }), headers: { 'Content-Type': 'application/json' } });
|
|
124
|
-
|
|
125
|
-
Object.assign(existingEN, {
|
|
148
|
+
|
|
149
|
+
Object.assign(existingEN, {
|
|
150
|
+
name: nameEN,
|
|
151
|
+
description: descriptionEN,
|
|
152
|
+
poem: poemEN,
|
|
153
|
+
status: 'idle',
|
|
154
|
+
});
|
|
126
155
|
await kvStore.put(kvKeyEN, JSON.stringify(existingEN));
|
|
127
|
-
|
|
128
|
-
Object.assign(existingPT, {
|
|
156
|
+
|
|
157
|
+
Object.assign(existingPT, {
|
|
158
|
+
name: namePT,
|
|
159
|
+
description: descriptionPT,
|
|
160
|
+
poem: poemPT,
|
|
161
|
+
status: 'idle',
|
|
162
|
+
});
|
|
129
163
|
await kvStore.put(kvKeyPT, JSON.stringify(existingPT));
|
|
130
|
-
|
|
131
|
-
console.log(
|
|
164
|
+
|
|
165
|
+
console.log(
|
|
166
|
+
`β
Stored basic info for ${conceptSlug}, combination: ${combinationString}.`
|
|
167
|
+
);
|
|
132
168
|
return;
|
|
133
169
|
} catch (error) {
|
|
134
|
-
console.error(
|
|
170
|
+
console.error(
|
|
171
|
+
`β Attempt ${attempts} failed at phase: ${phase}`,
|
|
172
|
+
error
|
|
173
|
+
);
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
}
|
|
@@ -164,10 +203,10 @@ export class ConceptService {
|
|
|
164
203
|
const enMatch = response.match(
|
|
165
204
|
/EN:\s*(?:β’\s*)?Name:\s*(.+?)\s*(?:β’\s*)?Description:\s*([\s\S]+?)\s*(?:β’\s*)?Poetic Passage:\s*([\s\S]+?)\s*(?=PT:|$)/
|
|
166
205
|
);
|
|
167
|
-
|
|
206
|
+
|
|
168
207
|
console.log('π Attempting to match Portuguese section...');
|
|
169
208
|
const ptMatch = response.match(
|
|
170
|
-
/PT:\s*(?:β’\s*)?Nome:\s*(.+?)\s*(?:β’\s*)?DescriΓ§Γ£o:\s*([\s\S]+?)\s*(?:β’\s*)?Passagem PoΓ©tica:\s*([\s\S]+)/
|
|
209
|
+
/PT:\s*(?:β’\s*)?Nome:\s*(.+?)\s*(?:β’\s*)?DescriΓ§Γ£o:\s*([\s\S]+?)\s*(?:β’\s*)?Passagem PoΓ©tica:\s*([\s\S]+)/
|
|
171
210
|
);
|
|
172
211
|
|
|
173
212
|
if (!enMatch || !ptMatch) {
|
|
@@ -808,361 +847,319 @@ export class ConceptService {
|
|
|
808
847
|
};
|
|
809
848
|
}
|
|
810
849
|
|
|
811
|
-
async regenerateDuplicateNames(limit?: number) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
async
|
|
946
|
-
conceptSlug: Concept,
|
|
947
|
-
limit?: number
|
|
948
|
-
) {
|
|
850
|
+
// async regenerateDuplicateNames(limit?: number) {
|
|
851
|
+
// console.log(`π Processing duplicate names for regeneration...`);
|
|
852
|
+
|
|
853
|
+
// const { duplicateEntries, usedNamesEN, usedNamesPT } =
|
|
854
|
+
// await this.findDuplicateNamesAndUsedNames(limit);
|
|
855
|
+
|
|
856
|
+
// if (duplicateEntries.length === 0) {
|
|
857
|
+
// console.log(`π No duplicates found. Nothing to regenerate.`);
|
|
858
|
+
// return { message: 'No duplicates detected.' };
|
|
859
|
+
// }
|
|
860
|
+
|
|
861
|
+
// for (const { name: oldNameEN, combinations } of duplicateEntries) {
|
|
862
|
+
// for (let i = 1; i < combinations.length; i++) {
|
|
863
|
+
// const [_, lang, conceptSlug, combinationString] =
|
|
864
|
+
// combinations[i].split(':');
|
|
865
|
+
|
|
866
|
+
// if (!lang || !conceptSlug || !combinationString) {
|
|
867
|
+
// console.warn(`β οΈ Invalid KV key format: ${combinations[i]}`);
|
|
868
|
+
// continue;
|
|
869
|
+
// }
|
|
870
|
+
|
|
871
|
+
// const kvKeyEN = buildConceptKVKey(
|
|
872
|
+
// 'en-us',
|
|
873
|
+
// conceptSlug as Concept,
|
|
874
|
+
// combinationString
|
|
875
|
+
// );
|
|
876
|
+
// const kvKeyPT = buildConceptKVKey(
|
|
877
|
+
// 'pt-br',
|
|
878
|
+
// conceptSlug as Concept,
|
|
879
|
+
// combinationString
|
|
880
|
+
// );
|
|
881
|
+
|
|
882
|
+
// const kvDataEN = (await this.context.env.KV_CONCEPTS.get(
|
|
883
|
+
// kvKeyEN,
|
|
884
|
+
// 'json'
|
|
885
|
+
// )) as {
|
|
886
|
+
// name: string;
|
|
887
|
+
// description: string;
|
|
888
|
+
// };
|
|
889
|
+
// const kvDataPT = (await this.context.env.KV_CONCEPTS.get(
|
|
890
|
+
// kvKeyPT,
|
|
891
|
+
// 'json'
|
|
892
|
+
// )) as {
|
|
893
|
+
// name: string;
|
|
894
|
+
// };
|
|
895
|
+
|
|
896
|
+
// console.log(
|
|
897
|
+
// `π Regenerating name for: ${kvKeyEN} (Old Name: "${oldNameEN}")`
|
|
898
|
+
// );
|
|
899
|
+
|
|
900
|
+
// let newNameEN: string | null = null;
|
|
901
|
+
// let newNamePT: string | null = null;
|
|
902
|
+
// let attempts = 0;
|
|
903
|
+
// const maxAttempts = 3;
|
|
904
|
+
|
|
905
|
+
// while (attempts < maxAttempts) {
|
|
906
|
+
// attempts++;
|
|
907
|
+
|
|
908
|
+
// const messages = this.context
|
|
909
|
+
// .buildLLMMessages()
|
|
910
|
+
// .regenerateConceptName({
|
|
911
|
+
// oldNameEN: kvDataEN.name,
|
|
912
|
+
// description: kvDataEN.description,
|
|
913
|
+
// usedNamesEN: Array.from(usedNamesEN), // β
Ensure array format
|
|
914
|
+
// });
|
|
915
|
+
|
|
916
|
+
// const aiResponse = await this.context
|
|
917
|
+
// .api()
|
|
918
|
+
// .callTogether.single(messages, {});
|
|
919
|
+
// if (!aiResponse) {
|
|
920
|
+
// console.warn(
|
|
921
|
+
// `β οΈ AI failed to generate a new name for ${kvKeyEN}, skipping.`
|
|
922
|
+
// );
|
|
923
|
+
// break;
|
|
924
|
+
// }
|
|
925
|
+
|
|
926
|
+
// // β
Parse AI response to extract both EN and PT names
|
|
927
|
+
// const [generatedEN, generatedPT] = aiResponse
|
|
928
|
+
// .split('\n')
|
|
929
|
+
// .map((s) => s.trim());
|
|
930
|
+
|
|
931
|
+
// // β
Validate that the generated names are not duplicates
|
|
932
|
+
// if (!usedNamesEN.has(generatedEN) && !usedNamesPT.has(generatedPT)) {
|
|
933
|
+
// newNameEN = generatedEN;
|
|
934
|
+
// newNamePT = generatedPT;
|
|
935
|
+
// break; // β
Found unique names, exit retry loop
|
|
936
|
+
// }
|
|
937
|
+
|
|
938
|
+
// console.warn(
|
|
939
|
+
// `β οΈ AI suggested a duplicate name: EN - "${generatedEN}", PT - "${generatedPT}". Retrying... (Attempt ${attempts}/${maxAttempts})`
|
|
940
|
+
// );
|
|
941
|
+
// }
|
|
942
|
+
|
|
943
|
+
// if (!newNameEN || !newNamePT) {
|
|
944
|
+
// console.error(
|
|
945
|
+
// `π¨ Failed to generate a unique name for ${kvKeyEN} after ${maxAttempts} attempts.`
|
|
946
|
+
// );
|
|
947
|
+
// continue;
|
|
948
|
+
// }
|
|
949
|
+
|
|
950
|
+
// console.log(
|
|
951
|
+
// `β
Storing new names for ${kvKeyEN}: EN - "${newNameEN}", PT - "${newNamePT}"`
|
|
952
|
+
// );
|
|
953
|
+
|
|
954
|
+
// // β
Update KV with new names (EN)
|
|
955
|
+
// Object.assign(kvDataEN, { name: newNameEN });
|
|
956
|
+
// await this.context.env.KV_CONCEPTS.put(
|
|
957
|
+
// kvKeyEN,
|
|
958
|
+
// JSON.stringify(kvDataEN)
|
|
959
|
+
// );
|
|
960
|
+
|
|
961
|
+
// // β
Update KV with new names (PT)
|
|
962
|
+
// Object.assign(kvDataPT, { name: newNamePT });
|
|
963
|
+
// await this.context.env.KV_CONCEPTS.put(
|
|
964
|
+
// kvKeyPT,
|
|
965
|
+
// JSON.stringify(kvDataPT)
|
|
966
|
+
// );
|
|
967
|
+
|
|
968
|
+
// // β
Add new names to the used names sets to prevent future reuse
|
|
969
|
+
// usedNamesEN.add(newNameEN);
|
|
970
|
+
// usedNamesPT.add(newNamePT);
|
|
971
|
+
|
|
972
|
+
// // β
Stop if limit is reached
|
|
973
|
+
// if (limit && --limit <= 0) {
|
|
974
|
+
// console.log(`π― Limit reached, stopping further processing.`);
|
|
975
|
+
// return { message: 'Regeneration limit reached.' };
|
|
976
|
+
// }
|
|
977
|
+
// }
|
|
978
|
+
// }
|
|
979
|
+
|
|
980
|
+
// console.log(`π Duplicate names regenerated successfully.`);
|
|
981
|
+
// return { message: 'Duplicate names processed and regenerated.' };
|
|
982
|
+
// }
|
|
983
|
+
|
|
984
|
+
async findDuplicateNamesForConcept(conceptSlug: Concept) {
|
|
949
985
|
console.log(
|
|
950
|
-
`π
|
|
986
|
+
`π Fetching duplicate names from KV_CONCEPT_CACHE for ${conceptSlug}...`
|
|
951
987
|
);
|
|
952
988
|
|
|
953
|
-
const
|
|
954
|
-
let cursor: string | undefined = undefined;
|
|
955
|
-
let nameUsageMapEN: Record<string, string[]> = {}; // { enName: [combination1, combination2] }
|
|
956
|
-
let usedNamesEN = new Set<string>(); // β
All used EN names
|
|
957
|
-
let usedNamesPT = new Set<string>(); // β
All used PT names
|
|
958
|
-
let totalKeysScanned = 0;
|
|
959
|
-
|
|
960
|
-
do {
|
|
961
|
-
const response: KVNamespaceListResult<KVConcept> = (await kvStore.list({
|
|
962
|
-
prefix: `concepts:en-us:${conceptSlug}:`, // β
Fetch only keys for this concept
|
|
963
|
-
cursor,
|
|
964
|
-
})) as KVNamespaceListResult<KVConcept, string>;
|
|
965
|
-
|
|
966
|
-
const {
|
|
967
|
-
keys,
|
|
968
|
-
cursor: newCursor,
|
|
969
|
-
}: { keys: KVNamespaceListKey<KVConcept>[]; cursor?: string } = response;
|
|
970
|
-
|
|
971
|
-
totalKeysScanned += keys.length;
|
|
972
|
-
|
|
973
|
-
for (const key of keys) {
|
|
974
|
-
const kvDataEN = (await kvStore.get(key.name, 'json')) as {
|
|
975
|
-
name: string;
|
|
976
|
-
};
|
|
977
|
-
const kvKeyPT = key.name.replace(':en-us:', ':pt-br:'); // β
Get corresponding PT key
|
|
978
|
-
const kvDataPT = (await kvStore.get(kvKeyPT, 'json')) as {
|
|
979
|
-
name: string;
|
|
980
|
-
};
|
|
989
|
+
const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
|
|
981
990
|
|
|
982
|
-
|
|
983
|
-
|
|
991
|
+
// β
Fetch the cached concept name mappings
|
|
992
|
+
const nameMap: Record<string, string[]> =
|
|
993
|
+
(await kvCacheStore.get(
|
|
994
|
+
`cache:concepts:en-us:${conceptSlug}:0`,
|
|
995
|
+
'json'
|
|
996
|
+
)) || {};
|
|
984
997
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
nameUsageMapEN[kvDataEN.name].push(key.name); // Store the key for that name
|
|
989
|
-
}
|
|
998
|
+
let duplicateKeys: string[] = [];
|
|
999
|
+
let usedNamesEN = new Set<string>();
|
|
1000
|
+
let usedNamesPT = new Set<string>();
|
|
990
1001
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1002
|
+
for (const [name, keys] of Object.entries(nameMap)) {
|
|
1003
|
+
if (keys.length > 1) {
|
|
1004
|
+
duplicateKeys.push(...keys.slice(1)); // β
Store all but the first occurrence
|
|
994
1005
|
}
|
|
1006
|
+
usedNamesEN.add(name);
|
|
1007
|
+
}
|
|
995
1008
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
.map(([name, combinations]) => ({
|
|
1003
|
-
name,
|
|
1004
|
-
combinations,
|
|
1005
|
-
}));
|
|
1009
|
+
// β
Fetch PT-BR version from cache
|
|
1010
|
+
const nameMapPT: Record<string, string[]> =
|
|
1011
|
+
(await kvCacheStore.get(
|
|
1012
|
+
`cache:concepts:pt-br:${conceptSlug}:0`,
|
|
1013
|
+
'json'
|
|
1014
|
+
)) || {};
|
|
1006
1015
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
duplicateENNames = duplicateENNames.slice(0, limit);
|
|
1016
|
+
for (const name of Object.keys(nameMapPT)) {
|
|
1017
|
+
usedNamesPT.add(name);
|
|
1010
1018
|
}
|
|
1011
1019
|
|
|
1012
1020
|
console.log(
|
|
1013
|
-
`β
Found ${
|
|
1021
|
+
`β
Found ${duplicateKeys.length} duplicate keys for ${conceptSlug}.`
|
|
1014
1022
|
);
|
|
1015
1023
|
|
|
1016
1024
|
return {
|
|
1017
|
-
|
|
1018
|
-
usedNamesEN, // β
Set of all
|
|
1019
|
-
usedNamesPT, // β
Set of all
|
|
1025
|
+
duplicateKeys, // β
List of keys that need new names
|
|
1026
|
+
usedNamesEN, // β
Set of all used EN names
|
|
1027
|
+
usedNamesPT, // β
Set of all used PT names
|
|
1020
1028
|
};
|
|
1021
1029
|
}
|
|
1022
1030
|
|
|
1023
|
-
async regenerateDuplicateNamesForConcept(
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
);
|
|
1031
|
+
async regenerateDuplicateNamesForConcept(conceptSlug: Concept) {
|
|
1032
|
+
console.log(`π [START] Regenerating duplicate names for concept: ${conceptSlug}`);
|
|
1033
|
+
|
|
1034
|
+
const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
|
|
1035
|
+
const kvStore = this.context.env.KV_CONCEPTS;
|
|
1036
|
+
|
|
1037
|
+
// β
Get duplicate keys and used names
|
|
1038
|
+
const { duplicateKeys, usedNamesEN, usedNamesPT } =
|
|
1039
|
+
await this.findDuplicateNamesForConcept(conceptSlug);
|
|
1040
|
+
|
|
1041
|
+
if (duplicateKeys.length === 0) {
|
|
1042
|
+
console.log(`π No duplicates found for ${conceptSlug}.`);
|
|
1036
1043
|
return { message: `No duplicates detected for ${conceptSlug}.` };
|
|
1037
1044
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1045
|
+
|
|
1046
|
+
let updatedKeys: string[] = [];
|
|
1047
|
+
let recentDuplicates = new Set<string>(); // β
Track newly generated names dynamically
|
|
1048
|
+
|
|
1049
|
+
console.log(`π Found ${duplicateKeys.length} duplicate keys to process.`);
|
|
1050
|
+
|
|
1051
|
+
for (const kvKeyEN of duplicateKeys) {
|
|
1052
|
+
const kvKeyPT = kvKeyEN.replace(':en-us:', ':pt-br:');
|
|
1053
|
+
|
|
1054
|
+
const kvDataEN = (await kvStore.get(kvKeyEN, 'json')) as {
|
|
1055
|
+
name: string;
|
|
1056
|
+
description: string;
|
|
1057
|
+
};
|
|
1058
|
+
const kvDataPT = (await kvStore.get(kvKeyPT, 'json')) as { name: string };
|
|
1059
|
+
|
|
1060
|
+
if (!kvDataEN || !kvDataEN.name) {
|
|
1061
|
+
console.warn(`β οΈ Missing data for key: ${kvKeyEN}, skipping.`);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
console.log(`π Processing duplicate: ${kvKeyEN} (Old Name: "${kvDataEN.name}")`);
|
|
1066
|
+
|
|
1067
|
+
let newNameEN: string | null = null;
|
|
1068
|
+
let newNamePT: string | null = null;
|
|
1069
|
+
let attempts = 0;
|
|
1070
|
+
const maxAttempts = 3;
|
|
1071
|
+
|
|
1072
|
+
while (attempts < maxAttempts) {
|
|
1073
|
+
attempts++;
|
|
1074
|
+
|
|
1075
|
+
console.log(`π‘ Attempt ${attempts}/${maxAttempts} - Requesting AI for new name...`);
|
|
1076
|
+
|
|
1077
|
+
// β
Generate a new name, passing dynamically tracked duplicates
|
|
1078
|
+
const messages = this.context.buildLLMMessages().regenerateConceptName({
|
|
1079
|
+
conceptSlug,
|
|
1080
|
+
oldNameEN: kvDataEN.name,
|
|
1081
|
+
description: kvDataEN.description,
|
|
1082
|
+
recentNames: Array.from(recentDuplicates), // β
Prevent reusing recent names
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
const aiResponse = await this.context.api().callTogether.single(messages, {});
|
|
1086
|
+
|
|
1087
|
+
if (!aiResponse) {
|
|
1088
|
+
console.warn(`β οΈ AI failed to generate a new name for ${kvKeyEN}, skipping.`);
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// β
Parse AI response
|
|
1093
|
+
const [generatedEN, generatedPT] = aiResponse.split('\n').map((s) => s.trim());
|
|
1094
|
+
|
|
1095
|
+
console.log(`π¨ AI Response: EN - "${generatedEN}", PT - "${generatedPT}"`);
|
|
1096
|
+
|
|
1097
|
+
// β
Check if the name already exists
|
|
1044
1098
|
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
concept !== conceptSlug
|
|
1099
|
+
usedNamesEN.has(generatedEN) ||
|
|
1100
|
+
usedNamesPT.has(generatedPT) ||
|
|
1101
|
+
recentDuplicates.has(generatedEN)
|
|
1049
1102
|
) {
|
|
1050
|
-
console.warn(`β οΈ Invalid KV key format: ${combinations[i]}`);
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const kvKeyEN = buildConceptKVKey(
|
|
1055
|
-
'en-us',
|
|
1056
|
-
concept as Concept,
|
|
1057
|
-
combinationString
|
|
1058
|
-
);
|
|
1059
|
-
const kvKeyPT = buildConceptKVKey(
|
|
1060
|
-
'pt-br',
|
|
1061
|
-
concept as Concept,
|
|
1062
|
-
combinationString
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
|
-
const kvDataEN = (await this.context.env.KV_CONCEPTS.get(
|
|
1066
|
-
kvKeyEN,
|
|
1067
|
-
'json'
|
|
1068
|
-
)) as {
|
|
1069
|
-
name: string;
|
|
1070
|
-
description: string;
|
|
1071
|
-
};
|
|
1072
|
-
const kvDataPT = (await this.context.env.KV_CONCEPTS.get(
|
|
1073
|
-
kvKeyPT,
|
|
1074
|
-
'json'
|
|
1075
|
-
)) as {
|
|
1076
|
-
name: string;
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
console.log(
|
|
1080
|
-
`π Regenerating name for: ${kvKeyEN} (Old Name: "${oldNameEN}")`
|
|
1081
|
-
);
|
|
1082
|
-
|
|
1083
|
-
let newNameEN: string | null = null;
|
|
1084
|
-
let newNamePT: string | null = null;
|
|
1085
|
-
let attempts = 0;
|
|
1086
|
-
const maxAttempts = 3;
|
|
1087
|
-
|
|
1088
|
-
while (attempts < maxAttempts) {
|
|
1089
|
-
attempts++;
|
|
1090
|
-
|
|
1091
|
-
const messages = this.context
|
|
1092
|
-
.buildLLMMessages()
|
|
1093
|
-
.regenerateConceptName({
|
|
1094
|
-
oldNameEN: kvDataEN.name,
|
|
1095
|
-
description: kvDataEN.description,
|
|
1096
|
-
usedNamesEN: Array.from(usedNamesEN), // β
Ensure array format
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
const aiResponse = await this.context
|
|
1100
|
-
.api()
|
|
1101
|
-
.callTogether.single(messages, {});
|
|
1102
|
-
if (!aiResponse) {
|
|
1103
|
-
console.warn(
|
|
1104
|
-
`β οΈ AI failed to generate a new name for ${kvKeyEN}, skipping.`
|
|
1105
|
-
);
|
|
1106
|
-
break;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// β
Parse AI response to extract both EN and PT names
|
|
1110
|
-
const [generatedEN, generatedPT] = aiResponse
|
|
1111
|
-
.split('\n')
|
|
1112
|
-
.map((s) => s.trim());
|
|
1113
|
-
|
|
1114
|
-
// β
Validate that the generated names are not duplicates
|
|
1115
|
-
if (!usedNamesEN.has(generatedEN) && !usedNamesPT.has(generatedPT)) {
|
|
1116
|
-
newNameEN = generatedEN;
|
|
1117
|
-
newNamePT = generatedPT;
|
|
1118
|
-
break; // β
Found unique names, exit retry loop
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
1103
|
console.warn(
|
|
1122
|
-
`β οΈ
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if (!newNameEN || !newNamePT) {
|
|
1127
|
-
console.error(
|
|
1128
|
-
`π¨ Failed to generate a unique name for ${kvKeyEN} after ${maxAttempts} attempts.`
|
|
1104
|
+
`β οΈ Duplicate detected: EN - "${generatedEN}", PT - "${generatedPT}". Retrying...`
|
|
1129
1105
|
);
|
|
1106
|
+
recentDuplicates.add(generatedEN);
|
|
1107
|
+
recentDuplicates.add(generatedPT);
|
|
1130
1108
|
continue;
|
|
1131
1109
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
);
|
|
1136
|
-
|
|
1137
|
-
// β
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1110
|
+
|
|
1111
|
+
newNameEN = generatedEN;
|
|
1112
|
+
newNamePT = generatedPT;
|
|
1113
|
+
recentDuplicates.add(newNameEN);
|
|
1114
|
+
recentDuplicates.add(newNamePT);
|
|
1115
|
+
break; // β
Found unique names, exit retry loop
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (!newNameEN || !newNamePT) {
|
|
1119
|
+
console.error(`π¨ Failed to generate a unique name for ${kvKeyEN} after ${maxAttempts} attempts.`);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
console.log(`β
Storing new names: EN - "${newNameEN}", PT - "${newNamePT}"`);
|
|
1124
|
+
|
|
1125
|
+
// β
Update KV with new names (EN)
|
|
1126
|
+
Object.assign(kvDataEN, { name: newNameEN });
|
|
1127
|
+
await kvStore.put(kvKeyEN, JSON.stringify(kvDataEN));
|
|
1128
|
+
|
|
1129
|
+
// β
Update KV with new names (PT)
|
|
1130
|
+
Object.assign(kvDataPT, { name: newNamePT });
|
|
1131
|
+
await kvStore.put(kvKeyPT, JSON.stringify(kvDataPT));
|
|
1132
|
+
|
|
1133
|
+
// β
Add new names to used names sets
|
|
1134
|
+
usedNamesEN.add(newNameEN);
|
|
1135
|
+
usedNamesPT.add(newNamePT);
|
|
1136
|
+
|
|
1137
|
+
// β
Collect successfully processed keys for cache update
|
|
1138
|
+
updatedKeys.push(kvKeyEN);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// β
Remove successfully updated keys from duplicate cache
|
|
1142
|
+
if (updatedKeys.length > 0) {
|
|
1143
|
+
let duplicateKeysList: string[] =
|
|
1144
|
+
(await kvCacheStore.get(`cache:duplicates:${conceptSlug}`, 'json')) || [];
|
|
1145
|
+
|
|
1146
|
+
console.log(`ποΈ Removing ${updatedKeys.length} resolved duplicates from cache.`);
|
|
1147
|
+
|
|
1148
|
+
// β
Remove processed keys
|
|
1149
|
+
duplicateKeysList = duplicateKeysList.filter((key) => !updatedKeys.includes(key));
|
|
1150
|
+
|
|
1151
|
+
if (duplicateKeysList.length === 0) {
|
|
1152
|
+
console.log(`β
All duplicates resolved for ${conceptSlug}, deleting cache entry.`);
|
|
1153
|
+
await kvCacheStore.delete(`cache:duplicates:${conceptSlug}`);
|
|
1154
|
+
} else {
|
|
1155
|
+
await kvCacheStore.put(
|
|
1156
|
+
`cache:duplicates:${conceptSlug}`,
|
|
1157
|
+
JSON.stringify(duplicateKeysList)
|
|
1149
1158
|
);
|
|
1150
|
-
|
|
1151
|
-
// β
Add new names to the used names sets to prevent future reuse
|
|
1152
|
-
usedNamesEN.add(newNameEN);
|
|
1153
|
-
usedNamesPT.add(newNamePT);
|
|
1154
|
-
|
|
1155
|
-
// β
Stop if limit is reached
|
|
1156
|
-
if (limit && --limit <= 0) {
|
|
1157
|
-
console.log(`π― Limit reached, stopping further processing.`);
|
|
1158
|
-
return { message: `Regeneration limit reached for ${conceptSlug}.` };
|
|
1159
|
-
}
|
|
1160
1159
|
}
|
|
1161
1160
|
}
|
|
1162
|
-
|
|
1163
|
-
console.log(
|
|
1164
|
-
`π Duplicate names regenerated successfully for concept: ${conceptSlug}.`
|
|
1165
|
-
);
|
|
1161
|
+
|
|
1162
|
+
console.log(`π [COMPLETE] Duplicate names regenerated successfully for concept: ${conceptSlug}.`);
|
|
1166
1163
|
return {
|
|
1167
1164
|
message: `Duplicate names processed and regenerated for ${conceptSlug}.`,
|
|
1168
1165
|
};
|
|
@@ -1224,4 +1221,58 @@ export class ConceptService {
|
|
|
1224
1221
|
message: `Cached ${totalKeysScanned} EN names for ${conceptSlug} in ${batchIndex} batches.`,
|
|
1225
1222
|
};
|
|
1226
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
async cacheDuplicateConcepts(conceptSlug: Concept) {
|
|
1226
|
+
console.log(`π Identifying duplicate entries for concept: ${conceptSlug}`);
|
|
1227
|
+
|
|
1228
|
+
const kvCacheStore = this.context.env.KV_CONCEPT_CACHE;
|
|
1229
|
+
let cursor: string | undefined = undefined;
|
|
1230
|
+
let batchIndex = 0;
|
|
1231
|
+
let duplicateKeys: string[] = [];
|
|
1232
|
+
|
|
1233
|
+
do {
|
|
1234
|
+
const batchKey = `cache:concepts:en-us:${conceptSlug}:${batchIndex}`;
|
|
1235
|
+
const batchData = await kvCacheStore.get<{ [name: string]: string[] }>(
|
|
1236
|
+
batchKey,
|
|
1237
|
+
'json'
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
if (!batchData) {
|
|
1241
|
+
console.log(`β οΈ No data found for batch: ${batchKey}, stopping.`);
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
console.log(
|
|
1246
|
+
`π¦ Processing batch ${batchIndex} with ${
|
|
1247
|
+
Object.keys(batchData).length
|
|
1248
|
+
} names`
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
for (const [name, keys] of Object.entries(batchData)) {
|
|
1252
|
+
if (keys.length > 1) {
|
|
1253
|
+
// Collect duplicate entries (all but the first one)
|
|
1254
|
+
duplicateKeys.push(...keys.slice(1));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
batchIndex++;
|
|
1259
|
+
} while (true);
|
|
1260
|
+
|
|
1261
|
+
if (duplicateKeys.length === 0) {
|
|
1262
|
+
console.log(`β
No duplicates found for ${conceptSlug}.`);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
console.log(
|
|
1267
|
+
`π¨ Found ${duplicateKeys.length} duplicate entries for ${conceptSlug}`
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// Store the duplicate keys list in KV
|
|
1271
|
+
await kvCacheStore.put(
|
|
1272
|
+
`cache:duplicates:${conceptSlug}`,
|
|
1273
|
+
JSON.stringify(duplicateKeys)
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
console.log(`β
Stored duplicate keys for ${conceptSlug} in cache.`);
|
|
1277
|
+
}
|
|
1227
1278
|
}
|
|
@@ -66,6 +66,17 @@ export class ConceptWorkflow {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
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
|
+
|
|
69
80
|
async processBatch(
|
|
70
81
|
conceptSlug: Concept,
|
|
71
82
|
combinations: string[],
|
package/package.json
CHANGED
package/utils/buildMessages.ts
CHANGED
|
@@ -183,42 +183,56 @@ export const buildLLMMessages = (env: BackendBindings) => ({
|
|
|
183
183
|
regenerateConceptName: ({
|
|
184
184
|
oldNameEN,
|
|
185
185
|
description,
|
|
186
|
-
|
|
186
|
+
conceptSlug,
|
|
187
|
+
recentNames = [],
|
|
187
188
|
}: {
|
|
188
189
|
oldNameEN: string;
|
|
189
190
|
description: string;
|
|
190
|
-
|
|
191
|
+
conceptSlug: string;
|
|
192
|
+
recentNames?: string[];
|
|
191
193
|
}): ChatMessages => [
|
|
192
194
|
{
|
|
193
195
|
role: 'system',
|
|
194
196
|
content: `
|
|
195
|
-
You are an expert in
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
You are an expert in mystical and symbolic naming. Your task is to generate a new, unique name for a given concept while ensuring it aligns with its description.
|
|
198
|
+
|
|
199
|
+
Guidelines:
|
|
200
|
+
β’ The name must have exactly six words.
|
|
201
|
+
β’ It must start with "The ${conceptSlug} of" in English and "O ${conceptSlug} de" in Portuguese.
|
|
202
|
+
β’ The last three words should be distinct, meaningful, and thematically relevant.
|
|
203
|
+
β’ It should reflect the conceptβs core essence such as protection, transformation, or resilience.
|
|
204
|
+
β’ Avoid generic, redundant, or overly abstract terms.
|
|
205
|
+
β’ Ensure it does not resemble the previous name.
|
|
206
|
+
β’ The name must not match any of the recently generated names.
|
|
207
|
+
|
|
208
|
+
Input:
|
|
209
|
+
β’ Concept Name: ${conceptSlug}
|
|
210
|
+
β’ Previous Name Already Used: ${oldNameEN}
|
|
211
|
+
β’ Recently Generated Names: ${
|
|
212
|
+
recentNames.length > 0 ? recentNames.join(', ') : 'None'
|
|
213
|
+
}
|
|
214
|
+
β’ Description: ${description}
|
|
215
|
+
|
|
216
|
+
Output Format:
|
|
217
|
+
|
|
218
|
+
EN:
|
|
219
|
+
β’ New Name: The ${conceptSlug} of [New Symbolic Concept]
|
|
220
|
+
|
|
221
|
+
PT:
|
|
222
|
+
β’ Novo Nome: O ${conceptSlug} de [Novo Conceito SimbΓ³lico]
|
|
223
|
+
|
|
224
|
+
No explanations or additional text. Return only the names.
|
|
210
225
|
`,
|
|
211
226
|
},
|
|
212
227
|
{
|
|
213
228
|
role: 'user',
|
|
214
229
|
content: `
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Also, provide a natural Portuguese translation of the new name.
|
|
230
|
+
β’ Concept Name: ${conceptSlug}
|
|
231
|
+
β’ Previous Name Already Used: ${oldNameEN}
|
|
232
|
+
β’ Recently Generated Names: ${
|
|
233
|
+
recentNames.length > 0 ? recentNames.join(', ') : 'None'
|
|
234
|
+
}
|
|
235
|
+
β’ Description: ${description}
|
|
222
236
|
`,
|
|
223
237
|
},
|
|
224
238
|
],
|