Spaces:
Sleeping
Sleeping
improve prompting tactics and retry logic for multiple tasks
Browse files- src/aiService.js +128 -112
src/aiService.js
CHANGED
@@ -20,6 +20,25 @@ class OpenRouterService {
|
|
20 |
this.apiKey = key;
|
21 |
}
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
async generateContextualHint(prompt) {
|
24 |
// Check for API key at runtime
|
25 |
const currentKey = this.getApiKey();
|
@@ -43,11 +62,10 @@ class OpenRouterService {
|
|
43 |
body: JSON.stringify({
|
44 |
model: this.model,
|
45 |
messages: [{
|
46 |
-
role: 'system',
|
47 |
-
content: 'You provide clues for word puzzles. You will be told the target word that players need to guess, but you must NEVER mention, spell, or reveal that word in your response. Follow the EXACT format requested. Be concise and direct about the target word without revealing it. Use plain text only - no bold, italics, asterisks, or markdown formatting. Stick to word limits.'
|
48 |
-
}, {
|
49 |
role: 'user',
|
50 |
-
content:
|
|
|
|
|
51 |
}],
|
52 |
max_tokens: 50,
|
53 |
temperature: 0.6
|
@@ -109,22 +127,20 @@ class OpenRouterService {
|
|
109 |
}
|
110 |
|
111 |
try {
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
role: 'user',
|
127 |
-
content: `Select exactly ${count} words from this passage for a cloze exercise.
|
128 |
|
129 |
REQUIREMENTS:
|
130 |
- Choose clear, properly-spelled words (no OCR errors like "andsatires")
|
@@ -136,47 +152,48 @@ REQUIREMENTS:
|
|
136 |
Return ONLY a JSON array of the selected words.
|
137 |
|
138 |
Passage: "${passage}"`
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
}
|
156 |
-
|
157 |
-
// Check if response has expected structure
|
158 |
-
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
159 |
-
console.error('Invalid word selection API response structure:', data);
|
160 |
-
throw new Error('API response missing expected content');
|
161 |
-
}
|
162 |
-
|
163 |
-
const content = data.choices[0].message.content.trim();
|
164 |
-
|
165 |
-
// Try to parse as JSON array
|
166 |
-
try {
|
167 |
-
const words = JSON.parse(content);
|
168 |
-
if (Array.isArray(words)) {
|
169 |
-
return words.slice(0, count);
|
170 |
}
|
171 |
-
|
172 |
-
//
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
}
|
177 |
-
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
} catch (error) {
|
181 |
console.error('Error selecting words with AI:', error);
|
182 |
throw error;
|
@@ -359,65 +376,64 @@ Return as JSON: {"passage1": {...}, "passage2": {...}}`
|
|
359 |
}
|
360 |
|
361 |
try {
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
})
|
382 |
-
});
|
383 |
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
|
|
418 |
} catch (error) {
|
419 |
console.error('Error getting contextualization:', error);
|
420 |
-
return
|
421 |
}
|
422 |
}
|
423 |
|
|
|
20 |
this.apiKey = key;
|
21 |
}
|
22 |
|
23 |
+
async retryRequest(requestFn, maxRetries = 3, delayMs = 1000) {
|
24 |
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
25 |
+
try {
|
26 |
+
return await requestFn();
|
27 |
+
} catch (error) {
|
28 |
+
console.log(`API request attempt ${attempt}/${maxRetries} failed:`, error.message);
|
29 |
+
|
30 |
+
if (attempt === maxRetries) {
|
31 |
+
throw error; // Final attempt failed, throw the error
|
32 |
+
}
|
33 |
+
|
34 |
+
// Wait before retrying, with exponential backoff
|
35 |
+
const delay = delayMs * Math.pow(2, attempt - 1);
|
36 |
+
console.log(`Retrying in ${delay}ms...`);
|
37 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
38 |
+
}
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
async generateContextualHint(prompt) {
|
43 |
// Check for API key at runtime
|
44 |
const currentKey = this.getApiKey();
|
|
|
62 |
body: JSON.stringify({
|
63 |
model: this.model,
|
64 |
messages: [{
|
|
|
|
|
|
|
65 |
role: 'user',
|
66 |
+
content: `You provide clues for word puzzles. You will be told the target word that players need to guess, but you must NEVER mention, spell, or reveal that word in your response. Follow the EXACT format requested. Be concise and direct about the target word without revealing it. Use plain text only - no bold, italics, asterisks, or markdown formatting. Stick to word limits.
|
67 |
+
|
68 |
+
${prompt}`
|
69 |
}],
|
70 |
max_tokens: 50,
|
71 |
temperature: 0.6
|
|
|
127 |
}
|
128 |
|
129 |
try {
|
130 |
+
return await this.retryRequest(async () => {
|
131 |
+
const response = await fetch(this.apiUrl, {
|
132 |
+
method: 'POST',
|
133 |
+
headers: {
|
134 |
+
'Content-Type': 'application/json',
|
135 |
+
'Authorization': `Bearer ${this.apiKey}`,
|
136 |
+
'HTTP-Referer': window.location.origin,
|
137 |
+
'X-Title': 'Cloze Reader'
|
138 |
+
},
|
139 |
+
body: JSON.stringify({
|
140 |
+
model: this.model,
|
141 |
+
messages: [{
|
142 |
+
role: 'user',
|
143 |
+
content: `You are a vocabulary selector for educational cloze exercises. Select exactly ${count} words from this passage for a cloze exercise.
|
|
|
|
|
144 |
|
145 |
REQUIREMENTS:
|
146 |
- Choose clear, properly-spelled words (no OCR errors like "andsatires")
|
|
|
152 |
Return ONLY a JSON array of the selected words.
|
153 |
|
154 |
Passage: "${passage}"`
|
155 |
+
}],
|
156 |
+
max_tokens: 100,
|
157 |
+
temperature: 0.3
|
158 |
+
})
|
159 |
+
});
|
160 |
|
161 |
+
if (!response.ok) {
|
162 |
+
throw new Error(`API request failed: ${response.status}`);
|
163 |
+
}
|
164 |
|
165 |
+
const data = await response.json();
|
166 |
+
|
167 |
+
// Check for OpenRouter error response
|
168 |
+
if (data.error) {
|
169 |
+
console.error('OpenRouter API error for word selection:', data.error);
|
170 |
+
throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
}
|
172 |
+
|
173 |
+
// Check if response has expected structure
|
174 |
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
175 |
+
console.error('Invalid word selection API response structure:', data);
|
176 |
+
throw new Error('API response missing expected content');
|
177 |
}
|
178 |
+
|
179 |
+
const content = data.choices[0].message.content.trim();
|
180 |
+
|
181 |
+
// Try to parse as JSON array
|
182 |
+
try {
|
183 |
+
const words = JSON.parse(content);
|
184 |
+
if (Array.isArray(words)) {
|
185 |
+
return words.slice(0, count);
|
186 |
+
}
|
187 |
+
} catch (e) {
|
188 |
+
// If not valid JSON, try to extract words from the response
|
189 |
+
const matches = content.match(/"([^"]+)"/g);
|
190 |
+
if (matches) {
|
191 |
+
return matches.map(m => m.replace(/"/g, '')).slice(0, count);
|
192 |
+
}
|
193 |
+
}
|
194 |
+
|
195 |
+
throw new Error('Failed to parse AI response');
|
196 |
+
});
|
197 |
} catch (error) {
|
198 |
console.error('Error selecting words with AI:', error);
|
199 |
throw error;
|
|
|
376 |
}
|
377 |
|
378 |
try {
|
379 |
+
return await this.retryRequest(async () => {
|
380 |
+
const response = await fetch(this.apiUrl, {
|
381 |
+
method: 'POST',
|
382 |
+
headers: {
|
383 |
+
'Content-Type': 'application/json',
|
384 |
+
'Authorization': `Bearer ${this.apiKey}`,
|
385 |
+
'HTTP-Referer': window.location.origin,
|
386 |
+
'X-Title': 'Cloze Reader'
|
387 |
+
},
|
388 |
+
body: JSON.stringify({
|
389 |
+
model: this.model,
|
390 |
+
messages: [{
|
391 |
+
role: 'user',
|
392 |
+
content: `You are a literary expert. Write one factual sentence about "${title}" by ${author}. Focus on what type of work it is, when it was written, or its historical significance. Be accurate and concise. Do not add fictional details or characters.`
|
393 |
+
}],
|
394 |
+
max_tokens: 80,
|
395 |
+
temperature: 0.2
|
396 |
+
})
|
397 |
+
});
|
|
|
|
|
398 |
|
399 |
+
if (!response.ok) {
|
400 |
+
const errorText = await response.text();
|
401 |
+
console.error('Contextualization API error:', response.status, errorText);
|
402 |
+
throw new Error(`API request failed: ${response.status}`);
|
403 |
+
}
|
404 |
|
405 |
+
const data = await response.json();
|
406 |
+
|
407 |
+
// Check for OpenRouter error response
|
408 |
+
if (data.error) {
|
409 |
+
console.error('OpenRouter API error for contextualization:', data.error);
|
410 |
+
throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
|
411 |
+
}
|
412 |
+
|
413 |
+
// Check if response has expected structure
|
414 |
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
415 |
+
console.error('Invalid contextualization API response structure:', data);
|
416 |
+
throw new Error('API response missing expected content');
|
417 |
+
}
|
418 |
+
|
419 |
+
let content = data.choices[0].message.content.trim();
|
420 |
+
|
421 |
+
// Clean up AI response artifacts
|
422 |
+
content = content
|
423 |
+
.replace(/^\s*["']|["']\s*$/g, '') // Remove leading/trailing quotes
|
424 |
+
.replace(/^\s*[:;]+\s*/, '') // Remove leading colons and semicolons
|
425 |
+
.replace(/\*+/g, '') // Remove asterisks (markdown bold/italic)
|
426 |
+
.replace(/_+/g, '') // Remove underscores (markdown)
|
427 |
+
.replace(/#+\s*/g, '') // Remove hash symbols (markdown headers)
|
428 |
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
429 |
+
.trim();
|
430 |
+
|
431 |
+
console.log('Contextualization received:', content);
|
432 |
+
return content;
|
433 |
+
});
|
434 |
} catch (error) {
|
435 |
console.error('Error getting contextualization:', error);
|
436 |
+
return `📜 Practice with literature from ${author}'s "${title}"`;
|
437 |
}
|
438 |
}
|
439 |
|