CodeCommunity commited on
Commit
f107de5
·
verified ·
1 Parent(s): 1c5d3c3

Update app/services/reviewer_service.py

Browse files
Files changed (1) hide show
  1. app/services/reviewer_service.py +51 -105
app/services/reviewer_service.py CHANGED
@@ -4,25 +4,22 @@ import re
4
  import hashlib
5
  import sqlite3
6
  import threading
7
- from typing import List, Dict
8
- from concurrent.futures import ThreadPoolExecutor, as_completed
9
  from app.core.model_loader import llm_engine
10
 
11
  logger = logging.getLogger(__name__)
12
-
13
  DB_PATH = "review_cache.db"
14
 
15
  class AIReviewerService:
16
  def __init__(self):
17
- self.max_concurrent_batches = 3
18
- self.batch_size = 10
19
  self.db_lock = threading.Lock()
20
  self._init_db()
21
 
22
  def _init_db(self):
23
- """Initializes SQLite cache table."""
24
  with self.db_lock:
25
- with sqlite3.connect(DB_PATH) as conn:
26
  cursor = conn.cursor()
27
  cursor.execute("""
28
  CREATE TABLE IF NOT EXISTS review_cache (
@@ -33,15 +30,13 @@ class AIReviewerService:
33
  conn.commit()
34
 
35
  def _generate_file_hash(self, file_obj) -> str:
36
- """Generates a unique SHA256 digest based on path and content."""
37
  content = file_obj.content or ""
38
  raw_payload = f"{file_obj.fileName}:{content}"
39
  return hashlib.sha256(raw_payload.encode("utf-8")).hexdigest()
40
 
41
  def _get_cached_result(self, file_hash: str) -> dict:
42
- """Thread-safe SQLite fetch."""
43
  with self.db_lock:
44
- with sqlite3.connect(DB_PATH) as conn:
45
  cursor = conn.cursor()
46
  cursor.execute("SELECT result_json FROM review_cache WHERE file_hash = ?", (file_hash,))
47
  row = cursor.fetchone()
@@ -53,11 +48,10 @@ class AIReviewerService:
53
  return None
54
 
55
  def _save_cached_results(self, hash_result_pairs: List[tuple]):
56
- """Thread-safe batch insert into SQLite."""
57
  if not hash_result_pairs:
58
  return
59
  with self.db_lock:
60
- with sqlite3.connect(DB_PATH) as conn:
61
  cursor = conn.cursor()
62
  cursor.executemany(
63
  "INSERT OR REPLACE INTO review_cache (file_hash, result_json) VALUES (?, ?)",
@@ -87,41 +81,32 @@ class AIReviewerService:
87
  logger.info(f"⚡ Served all {len(files)} files instantly from SQLite cache.")
88
  return results
89
 
90
- logger.info(f"SQLite cache hit: {len(files) - len(uncached_files)}/{len(files)} files. Reviewing {len(uncached_files)} uncached files.")
91
-
92
- # 2. Batch process uncached files via Gemini
93
  batches = [
94
  uncached_files[i : i + self.batch_size]
95
  for i in range(0, len(uncached_files), self.batch_size)
96
  ]
97
-
98
  new_results = []
99
- with ThreadPoolExecutor(max_workers=self.max_concurrent_batches) as pool:
100
- future_to_batch = {
101
- pool.submit(self._process_batch, batch): batch for batch in batches
102
- }
103
- for future in as_completed(future_to_batch):
104
- batch = future_to_batch[future]
105
- try:
106
- batch_results = future.result()
107
- new_results.extend(batch_results)
108
- except Exception as e:
109
- logger.error(f"Batch failure: {e}")
110
- for f_item in batch:
111
- new_results.append(
112
- {
113
- "fileName": f_item.fileName,
114
- "vulnerabilities": [],
115
- "improvement_suggestions": [],
116
- "metrics": {"complexity": 0, "maintainability": 0},
117
- "error": "analysis_failed",
118
- }
119
- )
120
 
121
  new_results_map = {item.get("fileName"): item for item in new_results}
122
  to_cache = []
123
 
124
- # 3. Assemble final response list and write to SQLite
125
  for idx, f in zip(uncached_indices, uncached_files):
126
  res = new_results_map.get(
127
  f.fileName,
@@ -133,11 +118,9 @@ class AIReviewerService:
133
  "error": "analysis_failed",
134
  },
135
  )
136
-
137
  if "error" not in res:
138
  file_hash = self._generate_file_hash(f)
139
  to_cache.append((file_hash, res))
140
-
141
  results[idx] = res
142
 
143
  self._save_cached_results(to_cache)
@@ -148,43 +131,27 @@ class AIReviewerService:
148
  file_names = []
149
  for f in batch:
150
  raw_content = f.content or ""
151
- minified_content = self._minify_code(raw_content[:5000], f.fileName)
152
  combined_code += f"\n--- FILE: {f.fileName} ---\n{minified_content}\n"
153
  file_names.append(f.fileName)
154
 
155
- prompt = f"""Analyze these {len(batch)} source files for vulnerabilities and quality.
156
  {combined_code}
157
- Instructions:
158
- 1. Identify severe security/logic issues as 'vulnerabilities' with a 'suggestion' fix.
159
- 2. If no vulnerabilities exist, give 1-2 concise 'improvement_suggestions'.
160
- 3. Assign maintainability and complexity metrics (1-10).
161
- 4. Keep descriptions short to maintain fast execution.
162
-
163
- Return JSON array (1 object per file):
164
  [
165
  {{
166
- "fileName": "exact/path/from/header",
167
- "vulnerabilities": [
168
- {{
169
- "type": "SQLi/Logic/etc",
170
- "line": 10,
171
- "description": "Short explanation",
172
- "suggestion": "Specific fix"
173
- }}
174
- ],
175
- "improvement_suggestions": ["Tip 1"],
176
  "metrics": {{"complexity": 3, "maintainability": 8}}
177
  }}
178
  ]"""
179
-
180
  batch_results = []
181
  try:
182
- response_text = llm_engine.generate(prompt, max_tokens=4096)
 
183
  parsed = self._parse_json(response_text)
184
-
185
- processed_map = {
186
- item.get("fileName"): item for item in parsed if isinstance(item, dict)
187
- }
188
 
189
  for fn in file_names:
190
  if fn in processed_map:
@@ -192,44 +159,31 @@ Return JSON array (1 object per file):
192
  res.setdefault("vulnerabilities", [])
193
  res.setdefault("improvement_suggestions", [])
194
  res.setdefault("metrics", {"complexity": 5, "maintainability": 5})
195
-
196
- for vuln in res["vulnerabilities"]:
197
- if "suggestion" not in vuln:
198
- vuln["suggestion"] = "Review implementation logic for safety."
199
  batch_results.append(res)
200
  else:
201
- batch_results.append(
202
- {
203
- "fileName": fn,
204
- "vulnerabilities": [],
205
- "improvement_suggestions": ["No immediate improvements identified."],
206
- "metrics": {"complexity": 1, "maintainability": 10},
207
- }
208
- )
209
- except Exception as e:
210
- logger.error(f"Batch processing error: {e}")
211
- error_str = str(e)
212
- is_config_error = any(
213
- x in error_str
214
- for x in ["QUOTA_EXCEEDED", "RATE_LIMIT_EXCEEDED", "MISSING_API_KEY", "GENERATION_FAILED"]
215
- )
216
- for fn in file_names:
217
- batch_results.append(
218
- {
219
  "fileName": fn,
220
  "vulnerabilities": [],
221
  "improvement_suggestions": [],
222
- "metrics": {"complexity": 0, "maintainability": 0},
223
- "error": error_str if is_config_error else "analysis_failed",
224
- }
225
- )
 
 
 
 
 
 
 
 
226
  return batch_results
227
 
228
  def _minify_code(self, code: str, filename: str) -> str:
229
  lines = code.split("\n")
230
  cleaned_lines = []
231
  is_python = filename.endswith(".py")
232
- is_js_style = filename.endswith((".js", ".ts", ".jsx", ".tsx", ".java", ".kt", ".c", ".cpp"))
233
 
234
  for line in lines:
235
  stripped = line.strip()
@@ -239,28 +193,20 @@ Return JSON array (1 object per file):
239
  continue
240
  if is_js_style and stripped.startswith(("//", "/*", "*")):
241
  continue
242
- if any(log in stripped for log in ["console.log", "print(", "logger.", "Log.d", "Log.e"]):
243
- continue
244
- if is_python and " #" in line:
245
- line = line.split(" #", 1)[0]
246
- if is_js_style and " //" in line:
247
- line = line.split(" //", 1)[0]
248
- if line.strip():
249
- cleaned_lines.append(line.rstrip())
250
 
251
- return "\n".join(cleaned_lines)
252
 
253
  def _parse_json(self, text: str):
254
  text = text.strip()
255
  if not text:
256
  return []
257
- text = re.sub(r"^[^[]*\[", "[", text)
258
- text = re.sub(r"\][^]]*$", "]", text)
259
  text = text.replace("```json", "").replace("```", "").strip()
260
  try:
261
  return json.loads(text)
262
- except json.JSONDecodeError as e:
263
- logger.warning(f"JSON Decode Error: {e}")
264
  return []
265
 
266
  service = AIReviewerService()
 
4
  import hashlib
5
  import sqlite3
6
  import threading
7
+ from typing import List
 
8
  from app.core.model_loader import llm_engine
9
 
10
  logger = logging.getLogger(__name__)
 
11
  DB_PATH = "review_cache.db"
12
 
13
  class AIReviewerService:
14
  def __init__(self):
15
+ self.batch_size = 3 # Reduced from 10 to speed up individual prompts
 
16
  self.db_lock = threading.Lock()
17
  self._init_db()
18
 
19
  def _init_db(self):
20
+ """Initializes SQLite cache table with proper timeout."""
21
  with self.db_lock:
22
+ with sqlite3.connect(DB_PATH, timeout=30.0) as conn:
23
  cursor = conn.cursor()
24
  cursor.execute("""
25
  CREATE TABLE IF NOT EXISTS review_cache (
 
30
  conn.commit()
31
 
32
  def _generate_file_hash(self, file_obj) -> str:
 
33
  content = file_obj.content or ""
34
  raw_payload = f"{file_obj.fileName}:{content}"
35
  return hashlib.sha256(raw_payload.encode("utf-8")).hexdigest()
36
 
37
  def _get_cached_result(self, file_hash: str) -> dict:
 
38
  with self.db_lock:
39
+ with sqlite3.connect(DB_PATH, timeout=30.0) as conn:
40
  cursor = conn.cursor()
41
  cursor.execute("SELECT result_json FROM review_cache WHERE file_hash = ?", (file_hash,))
42
  row = cursor.fetchone()
 
48
  return None
49
 
50
  def _save_cached_results(self, hash_result_pairs: List[tuple]):
 
51
  if not hash_result_pairs:
52
  return
53
  with self.db_lock:
54
+ with sqlite3.connect(DB_PATH, timeout=30.0) as conn:
55
  cursor = conn.cursor()
56
  cursor.executemany(
57
  "INSERT OR REPLACE INTO review_cache (file_hash, result_json) VALUES (?, ?)",
 
81
  logger.info(f"⚡ Served all {len(files)} files instantly from SQLite cache.")
82
  return results
83
 
84
+ # 2. Sequential batch processing (prevents LLM thread lock contention)
 
 
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
+ for batch in batches:
92
+ try:
93
+ batch_results = self._process_batch(batch)
94
+ new_results.extend(batch_results)
95
+ except Exception as e:
96
+ logger.error(f"Batch processing error: {e}")
97
+ for f_item in batch:
98
+ new_results.append({
99
+ "fileName": f_item.fileName,
100
+ "vulnerabilities": [],
101
+ "improvement_suggestions": [],
102
+ "metrics": {"complexity": 0, "maintainability": 0},
103
+ "error": "analysis_failed",
104
+ })
 
 
 
 
 
 
 
105
 
106
  new_results_map = {item.get("fileName"): item for item in new_results}
107
  to_cache = []
108
 
109
+ # 3. Assemble final response and update cache
110
  for idx, f in zip(uncached_indices, uncached_files):
111
  res = new_results_map.get(
112
  f.fileName,
 
118
  "error": "analysis_failed",
119
  },
120
  )
 
121
  if "error" not in res:
122
  file_hash = self._generate_file_hash(f)
123
  to_cache.append((file_hash, res))
 
124
  results[idx] = res
125
 
126
  self._save_cached_results(to_cache)
 
131
  file_names = []
132
  for f in batch:
133
  raw_content = f.content or ""
134
+ minified_content = self._minify_code(raw_content[:2000], f.fileName) # Reduced preview limit to 2000 chars
135
  combined_code += f"\n--- FILE: {f.fileName} ---\n{minified_content}\n"
136
  file_names.append(f.fileName)
137
 
138
+ prompt = f"""Analyze {len(batch)} files. Be ultra-concise.
139
  {combined_code}
140
+ Return ONLY valid JSON array:
 
 
 
 
 
 
141
  [
142
  {{
143
+ "fileName": "name",
144
+ "vulnerabilities": [{{"type": "type", "line": 1, "description": "short", "suggestion": "fix"}}],
145
+ "improvement_suggestions": ["tip"],
 
 
 
 
 
 
 
146
  "metrics": {{"complexity": 3, "maintainability": 8}}
147
  }}
148
  ]"""
 
149
  batch_results = []
150
  try:
151
+ # Capped token length to 1024 max per batch turn
152
+ response_text = llm_engine.generate(prompt, max_tokens=1024)
153
  parsed = self._parse_json(response_text)
154
+ processed_map = {item.get("fileName"): item for item in parsed if isinstance(item, dict)}
 
 
 
155
 
156
  for fn in file_names:
157
  if fn in processed_map:
 
159
  res.setdefault("vulnerabilities", [])
160
  res.setdefault("improvement_suggestions", [])
161
  res.setdefault("metrics", {"complexity": 5, "maintainability": 5})
 
 
 
 
162
  batch_results.append(res)
163
  else:
164
+ batch_results.append({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  "fileName": fn,
166
  "vulnerabilities": [],
167
  "improvement_suggestions": [],
168
+ "metrics": {"complexity": 1, "maintainability": 10},
169
+ })
170
+ except Exception as e:
171
+ logger.error(f"Batch processing error: {e}")
172
+ for fn in file_names:
173
+ batch_results.append({
174
+ "fileName": fn,
175
+ "vulnerabilities": [],
176
+ "improvement_suggestions": [],
177
+ "metrics": {"complexity": 0, "maintainability": 0},
178
+ "error": str(e),
179
+ })
180
  return batch_results
181
 
182
  def _minify_code(self, code: str, filename: str) -> str:
183
  lines = code.split("\n")
184
  cleaned_lines = []
185
  is_python = filename.endswith(".py")
186
+ is_js_style = filename.endswith((".js", ".ts", ".jsx", ".tsx"))
187
 
188
  for line in lines:
189
  stripped = line.strip()
 
193
  continue
194
  if is_js_style and stripped.startswith(("//", "/*", "*")):
195
  continue
196
+ cleaned_lines.append(stripped)
 
 
 
 
 
 
 
197
 
198
+ return "\n".join(cleaned_lines[:50]) # Cap at 50 lines max per file
199
 
200
  def _parse_json(self, text: str):
201
  text = text.strip()
202
  if not text:
203
  return []
204
+ text = re.sub(r"^[^\[]*\[", "[", text)
205
+ text = re.sub(r"\][^\]]*$", "]", text)
206
  text = text.replace("```json", "").replace("```", "").strip()
207
  try:
208
  return json.loads(text)
209
+ except json.JSONDecodeError:
 
210
  return []
211
 
212
  service = AIReviewerService()