Spaces:
Sleeping
Sleeping
Update app/services/reviewer_service.py
Browse files- app/services/reviewer_service.py +22 -10
app/services/reviewer_service.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import json
|
| 2 |
import logging
|
| 3 |
import re
|
|
@@ -19,7 +20,6 @@ class AIReviewerService:
|
|
| 19 |
self._init_db()
|
| 20 |
|
| 21 |
def _init_db(self):
|
| 22 |
-
"""Initializes SQLite cache table with proper timeout."""
|
| 23 |
with self.db_lock:
|
| 24 |
with sqlite3.connect(DB_PATH, timeout=30.0) as conn:
|
| 25 |
cursor = conn.cursor()
|
|
@@ -69,7 +69,6 @@ class AIReviewerService:
|
|
| 69 |
uncached_files = []
|
| 70 |
uncached_indices = []
|
| 71 |
|
| 72 |
-
# 1. Check SQLite cache
|
| 73 |
for idx, f in enumerate(files):
|
| 74 |
file_hash = self._generate_file_hash(f)
|
| 75 |
cached_res = self._get_cached_result(file_hash)
|
|
@@ -83,14 +82,14 @@ class AIReviewerService:
|
|
| 83 |
logger.info(f"⚡ Served all {len(files)} files instantly from SQLite cache.")
|
| 84 |
return results
|
| 85 |
|
| 86 |
-
# 2. Parallel Sub-Batch Processing
|
| 87 |
batches = [
|
| 88 |
uncached_files[i : i + self.batch_size]
|
| 89 |
for i in range(0, len(uncached_files), self.batch_size)
|
| 90 |
]
|
| 91 |
|
| 92 |
new_results = []
|
| 93 |
-
|
|
|
|
| 94 |
future_to_batch = {pool.submit(self._process_batch, batch): batch for batch in batches}
|
| 95 |
for future in as_completed(future_to_batch):
|
| 96 |
try:
|
|
@@ -105,13 +104,12 @@ class AIReviewerService:
|
|
| 105 |
"vulnerabilities": [],
|
| 106 |
"improvement_suggestions": [],
|
| 107 |
"metrics": {"complexity": 0, "maintainability": 0.0},
|
| 108 |
-
"error": str(e),
|
| 109 |
})
|
| 110 |
|
| 111 |
new_results_map = {item.get("fileName"): item for item in new_results if isinstance(item, dict)}
|
| 112 |
to_cache = []
|
| 113 |
|
| 114 |
-
# 3. Assemble final response and update cache
|
| 115 |
for idx, f in zip(uncached_indices, uncached_files):
|
| 116 |
res = new_results_map.get(
|
| 117 |
f.fileName,
|
|
@@ -131,6 +129,22 @@ class AIReviewerService:
|
|
| 131 |
self._save_cached_results(to_cache)
|
| 132 |
return results
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def _process_batch(self, batch: list) -> list:
|
| 135 |
combined_code = ""
|
| 136 |
file_names = []
|
|
@@ -156,7 +170,7 @@ Return ONLY valid JSON array:
|
|
| 156 |
]"""
|
| 157 |
batch_results = []
|
| 158 |
try:
|
| 159 |
-
response_text =
|
| 160 |
parsed = self._parse_json(response_text)
|
| 161 |
processed_map = {item.get("fileName"): item for item in parsed if isinstance(item, dict)}
|
| 162 |
|
|
@@ -182,7 +196,7 @@ Return ONLY valid JSON array:
|
|
| 182 |
"vulnerabilities": [],
|
| 183 |
"improvement_suggestions": [],
|
| 184 |
"metrics": {"complexity": 0, "maintainability": 0.0},
|
| 185 |
-
"error": str(e),
|
| 186 |
})
|
| 187 |
return batch_results
|
| 188 |
|
|
@@ -209,9 +223,7 @@ Return ONLY valid JSON array:
|
|
| 209 |
if not text:
|
| 210 |
return []
|
| 211 |
|
| 212 |
-
# Strip markdown code blocks safely without corrupting string content
|
| 213 |
text = re.sub(r"```(?:json)?", "", text, flags=re.IGNORECASE).strip()
|
| 214 |
-
|
| 215 |
start_idx = text.find('[')
|
| 216 |
end_idx = text.rfind(']')
|
| 217 |
|
|
|
|
| 1 |
+
import time
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import re
|
|
|
|
| 20 |
self._init_db()
|
| 21 |
|
| 22 |
def _init_db(self):
|
|
|
|
| 23 |
with self.db_lock:
|
| 24 |
with sqlite3.connect(DB_PATH, timeout=30.0) as conn:
|
| 25 |
cursor = conn.cursor()
|
|
|
|
| 69 |
uncached_files = []
|
| 70 |
uncached_indices = []
|
| 71 |
|
|
|
|
| 72 |
for idx, f in enumerate(files):
|
| 73 |
file_hash = self._generate_file_hash(f)
|
| 74 |
cached_res = self._get_cached_result(file_hash)
|
|
|
|
| 82 |
logger.info(f"⚡ Served all {len(files)} files instantly from SQLite cache.")
|
| 83 |
return results
|
| 84 |
|
|
|
|
| 85 |
batches = [
|
| 86 |
uncached_files[i : i + self.batch_size]
|
| 87 |
for i in range(0, len(uncached_files), self.batch_size)
|
| 88 |
]
|
| 89 |
|
| 90 |
new_results = []
|
| 91 |
+
# Reduced max_workers to 2 to prevent hitting API Rate Limits
|
| 92 |
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
| 93 |
future_to_batch = {pool.submit(self._process_batch, batch): batch for batch in batches}
|
| 94 |
for future in as_completed(future_to_batch):
|
| 95 |
try:
|
|
|
|
| 104 |
"vulnerabilities": [],
|
| 105 |
"improvement_suggestions": [],
|
| 106 |
"metrics": {"complexity": 0, "maintainability": 0.0},
|
| 107 |
+
"error": "RATE_LIMIT_EXCEEDED" if "429" in str(e) else str(e),
|
| 108 |
})
|
| 109 |
|
| 110 |
new_results_map = {item.get("fileName"): item for item in new_results if isinstance(item, dict)}
|
| 111 |
to_cache = []
|
| 112 |
|
|
|
|
| 113 |
for idx, f in zip(uncached_indices, uncached_files):
|
| 114 |
res = new_results_map.get(
|
| 115 |
f.fileName,
|
|
|
|
| 129 |
self._save_cached_results(to_cache)
|
| 130 |
return results
|
| 131 |
|
| 132 |
+
def _call_llm_with_retry(self, prompt: str, max_tokens: int = 2048, retries: int = 3) -> str:
|
| 133 |
+
"""Helper to retry LLM generation automatically when 429 rate limits occur."""
|
| 134 |
+
delay = 3.0
|
| 135 |
+
for attempt in range(retries):
|
| 136 |
+
try:
|
| 137 |
+
return llm_engine.generate(prompt, max_tokens=max_tokens)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
err_str = str(e).upper()
|
| 140 |
+
if "429" in err_str or "RATE" in err_str or "LIMIT" in err_str:
|
| 141 |
+
if attempt < retries - 1:
|
| 142 |
+
logger.warning(f"⚠️ Rate limited (429). Retrying in {delay}s... (Attempt {attempt + 1}/{retries})")
|
| 143 |
+
time.sleep(delay)
|
| 144 |
+
delay *= 2 # Exponential backoff
|
| 145 |
+
continue
|
| 146 |
+
raise e
|
| 147 |
+
|
| 148 |
def _process_batch(self, batch: list) -> list:
|
| 149 |
combined_code = ""
|
| 150 |
file_names = []
|
|
|
|
| 170 |
]"""
|
| 171 |
batch_results = []
|
| 172 |
try:
|
| 173 |
+
response_text = self._call_llm_with_retry(prompt, max_tokens=2048)
|
| 174 |
parsed = self._parse_json(response_text)
|
| 175 |
processed_map = {item.get("fileName"): item for item in parsed if isinstance(item, dict)}
|
| 176 |
|
|
|
|
| 196 |
"vulnerabilities": [],
|
| 197 |
"improvement_suggestions": [],
|
| 198 |
"metrics": {"complexity": 0, "maintainability": 0.0},
|
| 199 |
+
"error": "RATE_LIMIT_EXCEEDED" if "429" in str(e) else str(e),
|
| 200 |
})
|
| 201 |
return batch_results
|
| 202 |
|
|
|
|
| 223 |
if not text:
|
| 224 |
return []
|
| 225 |
|
|
|
|
| 226 |
text = re.sub(r"```(?:json)?", "", text, flags=re.IGNORECASE).strip()
|
|
|
|
| 227 |
start_idx = text.find('[')
|
| 228 |
end_idx = text.rfind(']')
|
| 229 |
|