milwright commited on
Commit
b97292d
·
1 Parent(s): 24aa1d4

improve prompting tactics and retry logic for multiple tasks

Browse files
Files changed (1) hide show
  1. 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: prompt
 
 
51
  }],
52
  max_tokens: 50,
53
  temperature: 0.6
@@ -109,22 +127,20 @@ class OpenRouterService {
109
  }
110
 
111
  try {
112
- const response = await fetch(this.apiUrl, {
113
- method: 'POST',
114
- headers: {
115
- 'Content-Type': 'application/json',
116
- 'Authorization': `Bearer ${this.apiKey}`,
117
- 'HTTP-Referer': window.location.origin,
118
- 'X-Title': 'Cloze Reader'
119
- },
120
- body: JSON.stringify({
121
- model: this.model,
122
- messages: [{
123
- role: 'system',
124
- content: 'You are a vocabulary selector for educational cloze exercises. Select meaningful, properly-spelled content words that appear exactly as written in the passage.'
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
- max_tokens: 100,
141
- temperature: 0.3
142
- })
143
- });
144
 
145
- if (!response.ok) {
146
- throw new Error(`API request failed: ${response.status}`);
147
- }
148
 
149
- const data = await response.json();
150
-
151
- // Check for OpenRouter error response
152
- if (data.error) {
153
- console.error('OpenRouter API error for word selection:', data.error);
154
- throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
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
- } catch (e) {
172
- // If not valid JSON, try to extract words from the response
173
- const matches = content.match(/"([^"]+)"/g);
174
- if (matches) {
175
- return matches.map(m => m.replace(/"/g, '')).slice(0, count);
176
  }
177
- }
178
-
179
- throw new Error('Failed to parse AI response');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const response = await fetch(this.apiUrl, {
363
- method: 'POST',
364
- headers: {
365
- 'Content-Type': 'application/json',
366
- 'Authorization': `Bearer ${this.apiKey}`,
367
- 'HTTP-Referer': window.location.origin,
368
- 'X-Title': 'Cloze Reader'
369
- },
370
- body: JSON.stringify({
371
- model: this.model,
372
- messages: [{
373
- role: 'system',
374
- content: 'You are a literary expert. Provide exactly 1 short, factual sentence about this classic work. Be accurate and concise. Do not add fictional details or characters.'
375
- }, {
376
- role: 'user',
377
- content: `Write one factual sentence about "${title}" by ${author}. Focus on what type of work it is, when it was written, or its historical significance.`
378
- }],
379
- max_tokens: 80,
380
- temperature: 0.2
381
- })
382
- });
383
 
384
- if (!response.ok) {
385
- const errorText = await response.text();
386
- console.error('Contextualization API error:', response.status, errorText);
387
- throw new Error(`API request failed: ${response.status}`);
388
- }
389
 
390
- const data = await response.json();
391
-
392
- // Check for OpenRouter error response
393
- if (data.error) {
394
- console.error('OpenRouter API error for contextualization:', data.error);
395
- throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
396
- }
397
-
398
- // Check if response has expected structure
399
- if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
400
- console.error('Invalid contextualization API response structure:', data);
401
- throw new Error('API response missing expected content');
402
- }
403
-
404
- let content = data.choices[0].message.content.trim();
405
-
406
- // Clean up AI response artifacts
407
- content = content
408
- .replace(/^\s*["']|["']\s*$/g, '') // Remove leading/trailing quotes
409
- .replace(/^\s*[:;]+\s*/, '') // Remove leading colons and semicolons
410
- .replace(/\*+/g, '') // Remove asterisks (markdown bold/italic)
411
- .replace(/_+/g, '') // Remove underscores (markdown)
412
- .replace(/#+\s*/g, '') // Remove hash symbols (markdown headers)
413
- .replace(/\s+/g, ' ') // Normalize whitespace
414
- .trim();
415
-
416
- console.log('Contextualization received:', content);
417
- return content;
 
418
  } catch (error) {
419
  console.error('Error getting contextualization:', error);
420
- return `📚 Practice with classic literature from ${author}'s "${title}"`;
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