QuantumChecker 0.2.9__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuantumChecker
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: A package to evaluate homework submissions in Python, SQL, PowerBI, and SSIS.
5
5
  Author: Qobiljon
6
6
  Author-email: qobiljonkhayrullayev@gmail.com
@@ -0,0 +1,230 @@
1
+ import logging
2
+ import os
3
+ import zipfile
4
+ from datetime import datetime
5
+ from typing import List, Dict
6
+ from .python_evaluator import PythonEvaluator
7
+ from .sql_evaluator import SQLEvaluator
8
+ from .powerbi_evaluator import PowerBIEvaluator
9
+ from .ssis_evaluator import SSISEvaluator
10
+ import asyncio
11
+
12
+ _logger_cache = {}
13
+
14
+ class HomeworkEvaluator:
15
+ EVALUATOR_REGISTRY = {
16
+ "python": PythonEvaluator,
17
+ "sql": SQLEvaluator,
18
+ "powerbi": PowerBIEvaluator,
19
+ "ssis": SSISEvaluator
20
+ }
21
+
22
+ EXTENSION_TO_TYPE = {
23
+ ".py": "python",
24
+ ".sql": "sql",
25
+ ".pbit": "powerbi",
26
+ ".pdf": "powerbi",
27
+ ".dtsx": "ssis",
28
+ ".DTSX": "ssis",
29
+ ".txt": "text",
30
+ ".md": "text"
31
+ }
32
+
33
+ API_NAME_MAPPING = {
34
+ "python": "Google Gemini API",
35
+ "sql": "Google Gemini API",
36
+ "powerbi": "Google Gemini API",
37
+ "ssis": "Google Gemini API",
38
+ "text": "Google Gemini API"
39
+ }
40
+
41
+ def __init__(self, log_level: int = logging.INFO):
42
+ self.log_level = log_level
43
+ self._successful_key_cache = {}
44
+ self._rate_limit_delay = {}
45
+ self._invalid_key_cache = set()
46
+ self._lock = asyncio.Lock()
47
+ self._last_request_time = None
48
+
49
+ def _get_logger(self, log_type: str) -> logging.Logger:
50
+ log_name = f"{log_type}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"
51
+ if log_name not in _logger_cache:
52
+ logger = logging.getLogger(log_name)
53
+ logger.setLevel(self.log_level)
54
+ if not logger.handlers:
55
+ handler = logging.StreamHandler()
56
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
57
+ logger.addHandler(handler)
58
+ _logger_cache[log_name] = logger
59
+ return _logger_cache[log_name]
60
+
61
+ def parse_questions(self, content: str) -> List[str]:
62
+ logger = self._get_logger("QuantumCheck.main")
63
+ questions = [q.strip() for q in content.split("\n\n") if q.strip()]
64
+ if not questions:
65
+ raise ValueError("No valid questions found in content")
66
+ return questions
67
+
68
+ def _detect_zip_content_type(self, zip_path: str, logger: logging.Logger) -> str:
69
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
70
+ extensions = {os.path.splitext(name)[1].lower() for name in zip_ref.namelist()}
71
+ file_types = [self.EXTENSION_TO_TYPE.get(ext, "text") for ext in extensions if ext]
72
+ if "python" in file_types:
73
+ return "python"
74
+ elif "sql" in file_types:
75
+ return "sql"
76
+ elif "powerbi" in file_types:
77
+ return "powerbi"
78
+ elif "ssis" in file_types:
79
+ return "ssis"
80
+ else:
81
+ return "text"
82
+
83
+ async def evaluate_from_content(
84
+ self,
85
+ question_content: str,
86
+ answer_path: str,
87
+ api_keys: List[str],
88
+ question_type: str,
89
+ retry_count: int = 0
90
+ ) -> Dict[str, any]:
91
+ async with self._lock:
92
+ now = datetime.now()
93
+ if self._last_request_time:
94
+ elapsed = (now - self._last_request_time).total_seconds()
95
+ if elapsed < 30:
96
+ await asyncio.sleep(30 - elapsed)
97
+ self._last_request_time = datetime.now()
98
+
99
+ try:
100
+ questions = self.parse_questions(question_content)
101
+ except ValueError as e:
102
+ logger = self._get_logger("QuantumCheck.main")
103
+ return {
104
+ "score": 0,
105
+ "feedback": f"Error parsing question content: {str(e)}",
106
+ "issues": [str(e)],
107
+ "recommendations": [],
108
+ "used_api_key_index": None,
109
+ "used_api_name": None
110
+ }
111
+
112
+ answer_path = answer_path.strip()
113
+ _, ext = os.path.splitext(answer_path)
114
+ ext = ext.lower()
115
+
116
+ if ext == ".zip":
117
+ logger = self._get_logger("zip")
118
+ file_type = self._detect_zip_content_type(answer_path, logger)
119
+ else:
120
+ file_type = self.EXTENSION_TO_TYPE.get(ext, "text")
121
+ logger = self._get_logger(file_type)
122
+
123
+ eval_type = question_type if question_type in self.EVALUATOR_REGISTRY else file_type
124
+
125
+ if not os.path.exists(answer_path):
126
+ return {
127
+ "score": 0,
128
+ "feedback": f"Answer file not found: {answer_path}",
129
+ "issues": [f"Answer file not found: {answer_path}"],
130
+ "recommendations": [],
131
+ "used_api_key_index": None,
132
+ "used_api_name": None
133
+ }
134
+
135
+ evaluator_class = self.EVALUATOR_REGISTRY.get(eval_type, PythonEvaluator)
136
+ last_error_messages = []
137
+
138
+ available_keys = [(i + 1, key) for i, key in enumerate(api_keys) if key not in self._invalid_key_cache]
139
+
140
+ cached_key_idx = self._successful_key_cache.get(eval_type)
141
+ if cached_key_idx is not None and cached_key_idx < len(api_keys):
142
+ cached_key = api_keys[cached_key_idx]
143
+ if cached_key not in self._invalid_key_cache:
144
+ available_keys.insert(0, (cached_key_idx + 1, cached_key))
145
+
146
+ if not available_keys:
147
+ return {
148
+ "score": 0,
149
+ "feedback": "No valid API keys available.",
150
+ "issues": ["All API keys are invalid or rate-limited."],
151
+ "recommendations": [],
152
+ "used_api_key_index": None,
153
+ "used_api_name": None
154
+ }
155
+
156
+ for idx, key in available_keys:
157
+ if key in self._rate_limit_delay:
158
+ delay_until = self._rate_limit_delay[key]
159
+ current_time = datetime.now()
160
+ delay_until_time = datetime.fromtimestamp(delay_until)
161
+ if current_time < delay_until_time:
162
+ continue
163
+ else:
164
+ del self._rate_limit_delay[key]
165
+
166
+ evaluator = evaluator_class(key)
167
+ api_name = getattr(evaluator, 'get_api_name', lambda: self.API_NAME_MAPPING.get(eval_type, "Unknown API"))()
168
+
169
+ try:
170
+ evaluation = evaluator.evaluate(questions, answer_path, temp_dir=f"temp_extract_{os.getpid()}_{idx}")
171
+ feedback = evaluation.get("feedback", "").lower()
172
+ issues = " ".join(evaluation.get("issues", [])).lower()
173
+
174
+ if any(phrase in feedback or phrase in issues for phrase in ["api key not valid", "api_key_invalid"]):
175
+ last_error_messages.append(f"API key #{idx} invalid.")
176
+ self._invalid_key_cache.add(key)
177
+ continue
178
+
179
+ if any(phrase in feedback or phrase in issues for phrase in ["429", "too many requests", "rate limit"]):
180
+ last_error_messages.append(f"API key #{idx} rate limited.")
181
+ self._rate_limit_delay[key] = datetime.now().timestamp() + 300
182
+ continue
183
+
184
+ if any(phrase in feedback or phrase in issues for phrase in ["503", "service unavailable"]):
185
+ last_error_messages.append(f"API key #{idx} service unavailable.")
186
+ self._rate_limit_delay[key] = datetime.now().timestamp() + 7200
187
+ continue
188
+
189
+ if evaluation.get("score", 0) == 0 and "evaluation not returned" in feedback:
190
+ last_error_messages.append(f"API key #{idx} returned invalid evaluation.")
191
+ continue
192
+
193
+ self._successful_key_cache[eval_type] = idx - 1
194
+ return {
195
+ "score": evaluation.get("score", 0),
196
+ "feedback": evaluation.get("feedback", "No feedback provided"),
197
+ "issues": evaluation.get("issues", []),
198
+ "recommendations": evaluation.get("recommendations", []),
199
+ "used_api_key_index": idx,
200
+ "used_api_name": api_name
201
+ }
202
+
203
+ except Exception as e:
204
+ last_error_messages.append(f"Exception with key #{idx}: {str(e)}")
205
+ if "429" in str(e) or "rate limit" in str(e).lower():
206
+ self._rate_limit_delay[key] = datetime.now().timestamp() + 300
207
+ elif "503" in str(e) or "service unavailable" in str(e).lower():
208
+ self._rate_limit_delay[key] = datetime.now().timestamp() + 7200
209
+ continue
210
+
211
+ if retry_count < 3 and self._rate_limit_delay:
212
+ next_available_ts = min(self._rate_limit_delay.values())
213
+ wait_time = max(0, next_available_ts - datetime.now().timestamp())
214
+ await asyncio.sleep(wait_time + 1)
215
+ return await self.evaluate_from_content(
216
+ question_content=question_content,
217
+ answer_path=answer_path,
218
+ api_keys=api_keys,
219
+ question_type=question_type,
220
+ retry_count=retry_count + 1
221
+ )
222
+
223
+ return {
224
+ "score": 0,
225
+ "feedback": "Evaluation failed with all API keys." if retry_count >= 3 else "All API keys are temporarily unavailable.",
226
+ "issues": last_error_messages if last_error_messages else ["All API keys failed to evaluate the submission."],
227
+ "recommendations": [],
228
+ "used_api_key_index": None,
229
+ "used_api_name": None
230
+ }
@@ -37,7 +37,7 @@ logging.basicConfig(
37
37
 
38
38
 
39
39
  class GeminiFlashModel:
40
- def __init__(self, api_key: str, model_name: str = "gemini-1.5-flash"):
40
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash"):
41
41
  api_key = os.getenv("GEMINI_API_KEY") or api_key
42
42
  if not api_key:
43
43
  raise ValueError("API key not found in .env file or environment variables.")
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  class GeminiFlashModel:
22
- def __init__(self, api_key: str, model_name: str = "gemini-1.5-flash"):
22
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash"):
23
23
  if not api_key:
24
24
  raise ValueError("API key is required.")
25
25
  self.api_key = api_key
@@ -202,10 +202,6 @@ class PythonEvaluator:
202
202
  f"Questions:\n{combined_questions}\n\nAnswers:\n{combined_answers}"
203
203
  )
204
204
 
205
- final_prompt = prompt_text_python(combined_raw_content)
206
- with open("combined_python_prompt.txt", "w", encoding="utf-8") as f:
207
- f.write(final_prompt)
208
-
209
205
  return self.model.evaluate(combined_raw_content)
210
206
  except Exception as e:
211
207
  logger.error(f"Failed to process answers from {answer_path}: {str(e)}")
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  class GeminiFlashModel:
22
- def __init__(self, api_key: str, model_name: str = "gemini-1.5-flash"):
22
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash"):
23
23
  if not api_key:
24
24
  raise ValueError("API key is required.")
25
25
  self.api_key = api_key
@@ -202,12 +202,6 @@ class SQLEvaluator:
202
202
  f"Questions:\n{combined_questions}\n\nAnswers:\n{combined_answers}"
203
203
  )
204
204
 
205
- final_prompt = prompt_text_sql(combined_raw_content)
206
-
207
- logger.info(
208
- "Saved full combined content and prompt to 'combined_sql_full.txt'"
209
- )
210
-
211
205
  return self.model.evaluate(combined_raw_content)
212
206
  except Exception as e:
213
207
  logger.error(f"Failed to process answers from {answer_path}: {str(e)}")
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
18
  class GeminiFlashModel:
19
- def __init__(self, api_key: str, model_name: str = "gemini-1.5-flash"):
19
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash"):
20
20
  if not api_key:
21
21
  raise ValueError("API key is required.")
22
22
  self.api_key = api_key
@@ -385,16 +385,8 @@ class SSISEvaluator:
385
385
  f"Issues:\n{', '.join(issues) if issues else 'None'}"
386
386
  )
387
387
 
388
- final_prompt = prompt_text_ssis(combined_raw_content)
389
388
 
390
- # Save final prompt to txt file for debugging
391
- with open("last_ssis_prompt.txt", "w", encoding="utf-8") as f:
392
- f.write(final_prompt)
393
- logger.debug("Generated prompt: %s", final_prompt[:500])
394
-
395
- # Evaluate using Gemini model
396
389
  result = self.model.evaluate(combined_raw_content)
397
- # Append parsing issues to result
398
390
  result["issues"] = result.get("issues", []) + issues
399
391
  return result
400
392
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuantumChecker
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: A package to evaluate homework submissions in Python, SQL, PowerBI, and SSIS.
5
5
  Author: Qobiljon
6
6
  Author-email: qobiljonkhayrullayev@gmail.com
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="QuantumChecker",
5
- version="0.2.9",
5
+ version="0.3.0",
6
6
  author="Qobiljon",
7
7
  author_email="qobiljonkhayrullayev@gmail.com",
8
8
  description="A package to evaluate homework submissions in Python, SQL, PowerBI, and SSIS.",
@@ -10,9 +10,9 @@ async def main():
10
10
  Q2: What is the difference between a list and a tuple in Python?
11
11
  """
12
12
 
13
-
14
13
  answer_path = "answer/python1.zip"
15
14
  api_keys = []
15
+
16
16
  question_type = "python"
17
17
 
18
18
  result = await evaluator.evaluate_from_content(
@@ -23,7 +23,8 @@ async def main():
23
23
  )
24
24
 
25
25
  print("Evaluation Result:")
26
- print(result)
26
+ print(result["score"])
27
+ print(result["feedback"])
27
28
 
28
29
 
29
30
  if __name__ == "__main__":
@@ -1,222 +0,0 @@
1
- import logging
2
- import os
3
- import zipfile
4
- import random
5
- from datetime import datetime
6
- from typing import List, Dict, Optional
7
- from .python_evaluator import PythonEvaluator
8
- from .sql_evaluator import SQLEvaluator
9
- from .powerbi_evaluator import PowerBIEvaluator
10
- from .ssis_evaluator import SSISEvaluator
11
- import asyncio
12
-
13
- _logger_cache = {}
14
-
15
- class HomeworkEvaluator:
16
- EVALUATOR_REGISTRY = {
17
- "python": PythonEvaluator,
18
- "sql": SQLEvaluator,
19
- "powerbi": PowerBIEvaluator,
20
- "ssis": SSISEvaluator
21
- }
22
-
23
- EXTENSION_TO_TYPE = {
24
- ".py": "python",
25
- ".sql": "sql",
26
- ".pbit": "powerbi",
27
- ".pdf": "powerbi",
28
- ".dtsx": "ssis",
29
- ".DTSX": "ssis",
30
- ".txt": "text",
31
- ".md": "text"
32
- }
33
-
34
- API_NAME_MAPPING = {
35
- "python": "Google Gemini API",
36
- "sql": "Google Gemini API",
37
- "powerbi": "Google Gemini API",
38
- "ssis": "Google Gemini API",
39
- "text": "Google Gemini API"
40
- }
41
-
42
- def __init__(self, log_level: int = logging.INFO):
43
- self.log_level = log_level
44
- self._successful_key_cache = {}
45
- self._rate_limit_delay = {} # Track delay per key
46
-
47
- def _get_logger(self, log_type: str) -> logging.Logger:
48
- log_name = f"{log_type}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"
49
- if log_name not in _logger_cache:
50
- logger = logging.getLogger(log_name)
51
- logger.setLevel(self.log_level)
52
- if not logger.handlers:
53
- handler = logging.StreamHandler()
54
- handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
55
- logger.addHandler(handler)
56
- _logger_cache[log_name] = logger
57
- return _logger_cache[log_name]
58
-
59
- def parse_questions(self, content: str) -> List[str]:
60
- logger = self._get_logger("QuantumCheck.main")
61
- questions = [q.strip() for q in content.split("\n\n") if q.strip()]
62
- logger.info(f"Parsed {len(questions)} questions from content")
63
- if not questions:
64
- raise ValueError("No valid questions found in content")
65
- return questions
66
-
67
- def _detect_zip_content_type(self, zip_path: str, logger: logging.Logger) -> str:
68
- try:
69
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
70
- extensions = {os.path.splitext(name)[1].lower() for name in zip_ref.namelist()}
71
- file_types = [self.EXTENSION_TO_TYPE.get(ext, "text") for ext in extensions if ext]
72
- logger.info(f"Detected extensions in ZIP {zip_path}: {extensions}, types: {file_types}")
73
- if "python" in file_types:
74
- logger.info(f"Selected file type: python from extension: .py in ZIP: {zip_path}")
75
- return "python"
76
- elif "sql" in file_types:
77
- logger.info(f"Selected file type: sql from extension: .sql in ZIP: {zip_path}")
78
- return "sql"
79
- elif "powerbi" in file_types:
80
- logger.info(f"Selected file type: powerbi from extension: .pbit or .pdf in ZIP: {zip_path}")
81
- return "powerbi"
82
- elif "ssis" in file_types:
83
- logger.info(f"Selected file type: ssis from extension: .dtsx in ZIP: {zip_path}")
84
- return "ssis"
85
- else:
86
- logger.info(f"Selected file type: text (default) in ZIP: {zip_path}")
87
- return "text"
88
- except zipfile.BadZipFile:
89
- logger.error(f"Invalid ZIP file: {zip_path}")
90
- raise ValueError(f"Invalid ZIP file: {zip_path}")
91
-
92
- async def evaluate_from_content(
93
- self,
94
- question_content: str,
95
- answer_path: str,
96
- api_keys: List[str],
97
- question_type: str
98
- ) -> Dict[str, any]:
99
- try:
100
- questions = self.parse_questions(question_content)
101
- except ValueError as e:
102
- logger = self._get_logger("QuantumCheck.main")
103
- logger.error("Failed to parse question content: %s", str(e))
104
- return {
105
- "score": 0,
106
- "feedback": f"Error parsing question content: {str(e)}",
107
- "issues": [str(e)],
108
- "recommendations": [],
109
- "used_api_key_index": None,
110
- "used_api_name": None
111
- }
112
-
113
- answer_path = answer_path.strip()
114
- _, ext = os.path.splitext(answer_path)
115
- ext = ext.lower()
116
-
117
- # Determine file type, prioritizing question_type for evaluator selection
118
- if ext == ".zip":
119
- logger = self._get_logger("zip")
120
- file_type = self._detect_zip_content_type(answer_path, logger)
121
- else:
122
- file_type = self.EXTENSION_TO_TYPE.get(ext, "text")
123
- logger = self._get_logger(file_type)
124
-
125
- # Use question_type if provided, else fallback to file_type
126
- eval_type = question_type if question_type in self.EVALUATOR_REGISTRY else file_type
127
- logger.info(f"Processing answer_path: {answer_path} with detected file type: {file_type}, evaluation type: {eval_type}")
128
-
129
- if not os.path.exists(answer_path):
130
- logger.error(f"Answer file not found: {answer_path}")
131
- return {
132
- "score": 0,
133
- "feedback": f"Answer file not found: {answer_path}",
134
- "issues": [f"Answer file not found: {answer_path}"],
135
- "recommendations": [],
136
- "used_api_key_index": None,
137
- "used_api_name": None
138
- }
139
-
140
- evaluator_class = self.EVALUATOR_REGISTRY.get(eval_type, PythonEvaluator)
141
- last_error_messages = []
142
-
143
- # Shuffle keys for load balancing
144
- key_order = [(i + 1, key) for i, key in enumerate(api_keys)]
145
- random.shuffle(key_order)
146
-
147
- # Try cached key with 30% probability to encourage rotation
148
- cached_key_idx = self._successful_key_cache.get(eval_type)
149
- if cached_key_idx is not None and cached_key_idx < len(api_keys) and random.random() < 0.3:
150
- key_order.insert(0, (cached_key_idx + 1, api_keys[cached_key_idx]))
151
-
152
- for idx, key in key_order:
153
- # Check rate limit delay
154
- if key in self._rate_limit_delay:
155
- delay_until = self._rate_limit_delay[key]
156
- current_time = datetime.now()
157
- delay_until_time = datetime.fromtimestamp(delay_until)
158
- if current_time < delay_until_time:
159
- logger.info(f"API key #{idx} is rate-limited until {delay_until_time}, skipping.")
160
- continue
161
- else:
162
- del self._rate_limit_delay[key]
163
-
164
- logger.info(f"Trying API key #{idx}")
165
- evaluator = evaluator_class(key)
166
- api_name = getattr(evaluator, 'get_api_name', lambda: self.API_NAME_MAPPING.get(eval_type, "Unknown API"))()
167
- logger.info(f"Using API: {api_name} for evaluation type: {eval_type}")
168
-
169
- try:
170
- evaluation = evaluator.evaluate(questions, answer_path, temp_dir=f"temp_extract_{os.getpid()}_{idx}")
171
-
172
- feedback = evaluation.get("feedback", "").lower()
173
- issues = " ".join(evaluation.get("issues", [])).lower()
174
-
175
- # Check for invalid API key
176
- if any(phrase in feedback or phrase in issues for phrase in ["api key not valid", "api_key_invalid"]):
177
- logger.warning(f"API key #{idx} invalid, trying next key.")
178
- last_error_messages.append(f"API key #{idx} invalid.")
179
- continue
180
-
181
- # Check for rate limit errors
182
- if any(phrase in feedback or phrase in issues for phrase in ["429", "too many requests", "rate limit"]):
183
- logger.warning(f"API key #{idx} hit rate limit, applying delay.")
184
- last_error_messages.append(f"API key #{idx} rate limited.")
185
- self._rate_limit_delay[key] = datetime.now().timestamp() + 45 # 45s delay
186
- continue
187
-
188
- # Check for invalid evaluation
189
- if evaluation.get("score", 0) == 0 and "evaluation not returned" in feedback:
190
- logger.warning(f"API key #{idx} returned invalid evaluation, trying next key.")
191
- last_error_messages.append(f"API key #{idx} returned invalid evaluation.")
192
- continue
193
-
194
- # Cache successful key
195
- self._successful_key_cache[eval_type] = idx - 1
196
- logger.info(f"Evaluation succeeded with API key #{idx}: Score = {evaluation.get('score')}")
197
-
198
- return {
199
- "score": evaluation.get("score", 0),
200
- "feedback": evaluation.get("feedback", "No feedback provided"),
201
- "issues": evaluation.get("issues", []),
202
- "recommendations": evaluation.get("recommendations", []),
203
- "used_api_key_index": idx,
204
- "used_api_name": api_name
205
- }
206
-
207
- except Exception as e:
208
- logger.error(f"Exception using API key #{idx}: {str(e)}")
209
- last_error_messages.append(f"Exception with key #{idx}: {str(e)}")
210
- if "429" in str(e) or "rate limit" in str(e).lower():
211
- self._rate_limit_delay[key] = datetime.now().timestamp() + 45
212
- continue
213
-
214
- logger.error("Evaluation failed with all API keys.")
215
- return {
216
- "score": 0,
217
- "feedback": "Evaluation failed with all API keys.",
218
- "issues": last_error_messages if last_error_messages else ["All API keys failed to evaluate the submission."],
219
- "recommendations": [],
220
- "used_api_key_index": None,
221
- "used_api_name": None
222
- }
File without changes
File without changes