CodeCommunity commited on
Commit
92def5d
·
verified ·
1 Parent(s): 67dc4f4

Update app/services/reviewer_service.py

Browse files
Files changed (1) hide show
  1. app/services/reviewer_service.py +170 -92
app/services/reviewer_service.py CHANGED
@@ -2,36 +2,84 @@ import json
2
  import logging
3
  import re
4
  from typing import List, Dict
 
5
  from app.core.model_loader import llm_engine
6
 
7
  logger = logging.getLogger(__name__)
8
 
 
9
  class AIReviewerService:
10
  def __init__(self):
11
- pass
 
 
 
 
12
 
13
  def review_batch_code(self, files: list) -> list:
14
- results = []
15
-
16
- # Process in batches of 5 to stay within Gemini Free Tier limits (15 RPM)
17
- batch_size = 5
18
-
19
- for i in range(0, len(files), batch_size):
20
- batch = files[i : i + batch_size]
21
- combined_code = ""
22
- file_names = []
23
-
24
- for f in batch:
25
- # Minify code to save tokens
26
- raw_content = f.content or ""
27
- # Limit to 6k chars per file to stay within context window safely
28
- minified_content = self._minify_code(raw_content[:6000], f.fileName)
29
-
30
- combined_code += f"\n--- FILE: {f.fileName} ---\n{minified_content}\n"
31
- file_names.append(f.fileName)
32
-
33
- # Updated prompt to explicitly request suggestions and improvements
34
- prompt = f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  Analyze the following {len(batch)} source code files for security vulnerabilities and code quality.
36
  {combined_code}
37
 
@@ -50,7 +98,7 @@ Output a JSON array (exactly 1 object per file):
50
  "type": "SQLi/Logic/etc",
51
  "line": 10,
52
  "description": "Short explanation",
53
- "suggestion": "Specific code fix"
54
  }}
55
  ],
56
  "improvement_suggestions": ["Tip 1", "Tip 2"],
@@ -58,87 +106,116 @@ Output a JSON array (exactly 1 object per file):
58
  }}
59
  ]
60
  """
61
- try:
62
- # 8k output tokens for the batch analysis
63
- response_text = llm_engine.generate(prompt, max_tokens=8192)
64
- batch_results = self._parse_json(response_text)
65
-
66
- # Map results by fileName for easy lookup
67
- processed_map = {item.get('fileName'): item for item in batch_results if isinstance(item, dict)}
68
-
69
- for fn in file_names:
70
- if fn in processed_map:
71
- res = processed_map[fn]
72
-
73
- # Ensure all required keys exist to prevent Android serialization errors
74
- res.setdefault("vulnerabilities", [])
75
- res.setdefault("improvement_suggestions", [])
76
- res.setdefault("metrics", {"complexity": 5, "maintainability": 5})
77
-
78
- # Ensure every vulnerability has a suggestion field
79
- for vuln in res["vulnerabilities"]:
80
- if "suggestion" not in vuln:
81
- vuln["suggestion"] = "Review the implementation logic for improved safety."
82
-
83
- results.append(res)
84
- else:
85
- # Fallback if the AI skipped a file in its response
86
- results.append({
87
- "fileName": fn,
88
- "vulnerabilities": [],
89
- "improvement_suggestions": ["No immediate improvements identified."],
90
- "metrics": {"complexity": 1, "maintainability": 10}
91
- })
92
-
93
- except Exception as e:
94
- logger.error(f"Batch processing error: {e}")
95
- error_str = str(e)
96
- is_config_error = any(x in error_str for x in [
97
- "QUOTA_EXCEEDED", "RATE_LIMIT_EXCEEDED", "MISSING_API_KEY", "GENERATION_FAILED"
98
- ])
99
- for fn in file_names:
100
- results.append({
101
- "fileName": fn,
102
- "vulnerabilities": [],
103
- "improvement_suggestions": [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  "metrics": {"complexity": 0, "maintainability": 0},
105
- "error": error_str if is_config_error else "analysis_failed"
106
- })
 
107
 
108
- return results
109
 
110
  def _minify_code(self, code: str, filename: str) -> str:
111
  """
112
  Removes comments, empty lines, and logs to optimize token usage.
113
  """
114
- lines = code.split('\n')
115
  cleaned_lines = []
116
-
117
- is_python = filename.endswith('.py')
118
- is_js_style = filename.endswith(('.js', '.ts', '.jsx', '.tsx', '.java', '.kt', '.c', '.cpp'))
119
-
 
 
120
  for line in lines:
121
  stripped = line.strip()
122
-
123
- if not stripped: continue
124
-
 
125
  # Skip comments
126
- if is_python and stripped.startswith('#'): continue
127
- if is_js_style and stripped.startswith(('//', '/*', '*')): continue
128
-
 
 
129
  # Skip common logging
130
- if any(log in stripped for log in ['console.log', 'print(', 'logger.', 'Log.d', 'Log.e']):
 
 
 
131
  continue
132
-
133
  # Strip inline comments
134
- if is_python and ' #' in line: line = line.split(' #', 1)[0]
135
- if is_js_style and ' //' in line: line = line.split(' //', 1)[0]
136
-
137
- if not line.strip(): continue
 
 
 
138
 
139
  cleaned_lines.append(line.rstrip())
140
-
141
- return '\n'.join(cleaned_lines)
142
 
143
  def _parse_json(self, text: str):
144
  """
@@ -147,17 +224,18 @@ Output a JSON array (exactly 1 object per file):
147
  text = text.strip()
148
  if not text:
149
  return []
150
-
151
  # Clean markdown formatting if present
152
- text = re.sub(r"^[^[]*\[", "[", text)
153
- text = re.sub(r"\][^]]*$", "]", text)
154
  text = text.replace("```json", "").replace("```", "").strip()
155
-
156
  try:
157
  return json.loads(text)
158
  except json.JSONDecodeError as e:
159
  logger.warning(f"JSON Decode Error: {e}")
160
  return []
161
 
 
162
  # Instantiate the service
163
  service = AIReviewerService()
 
2
  import logging
3
  import re
4
  from typing import List, Dict
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
  from app.core.model_loader import llm_engine
7
 
8
  logger = logging.getLogger(__name__)
9
 
10
+
11
  class AIReviewerService:
12
  def __init__(self):
13
+ # Keep worker count under the Gemini free-tier RPM limit (15/min).
14
+ # 3 workers gives real parallelism without immediately tripping 429s,
15
+ # since llm_engine already has its own RPM guard + backoff on top.
16
+ self.max_concurrent_batches = 3
17
+ self.batch_size = 5
18
 
19
  def review_batch_code(self, files: list) -> list:
20
+ """
21
+ Splits files into batches and reviews them CONCURRENTLY using a thread
22
+ pool, instead of the old sequential for-loop. Each batch still makes
23
+ one blocking call to llm_engine.generate(), but multiple batches are
24
+ now in flight at once.
25
+ """
26
+ batches = [
27
+ files[i : i + self.batch_size]
28
+ for i in range(0, len(files), self.batch_size)
29
+ ]
30
+
31
+ if not batches:
32
+ return []
33
+
34
+ results: list = []
35
+
36
+ with ThreadPoolExecutor(max_workers=self.max_concurrent_batches) as pool:
37
+ future_to_batch = {
38
+ pool.submit(self._process_batch, batch): batch for batch in batches
39
+ }
40
+
41
+ for future in as_completed(future_to_batch):
42
+ batch = future_to_batch[future]
43
+ try:
44
+ batch_results = future.result()
45
+ results.extend(batch_results)
46
+ except Exception as e:
47
+ # If a whole batch blew up unexpectedly (shouldn't normally
48
+ # happen since _process_batch catches its own errors), fall
49
+ # back to per-file error entries so the response shape
50
+ # stays consistent for the Android client.
51
+ logger.error(f"Unhandled batch failure: {e}")
52
+ for f in batch:
53
+ results.append(
54
+ {
55
+ "fileName": f.fileName,
56
+ "vulnerabilities": [],
57
+ "improvement_suggestions": [],
58
+ "metrics": {"complexity": 0, "maintainability": 0},
59
+ "error": "analysis_failed",
60
+ }
61
+ )
62
+
63
+ return results
64
+
65
+ def _process_batch(self, batch: list) -> list:
66
+ """
67
+ Processes a single batch of files: builds the prompt, calls Gemini,
68
+ parses the response, and normalizes each file's result. This is the
69
+ unit of work that now runs concurrently across threads.
70
+ """
71
+ combined_code = ""
72
+ file_names = []
73
+
74
+ for f in batch:
75
+ raw_content = f.content or ""
76
+ # Limit to 6k chars per file to stay within context window safely
77
+ minified_content = self._minify_code(raw_content[:6000], f.fileName)
78
+
79
+ combined_code += f"\n--- FILE: {f.fileName} ---\n{minified_content}\n"
80
+ file_names.append(f.fileName)
81
+
82
+ prompt = f"""
83
  Analyze the following {len(batch)} source code files for security vulnerabilities and code quality.
84
  {combined_code}
85
 
 
98
  "type": "SQLi/Logic/etc",
99
  "line": 10,
100
  "description": "Short explanation",
101
+ "suggestion": "Specific code fix"
102
  }}
103
  ],
104
  "improvement_suggestions": ["Tip 1", "Tip 2"],
 
106
  }}
107
  ]
108
  """
109
+ batch_results = []
110
+
111
+ try:
112
+ response_text = llm_engine.generate(prompt, max_tokens=8192)
113
+ parsed = self._parse_json(response_text)
114
+
115
+ # Map results by fileName for easy lookup
116
+ processed_map = {
117
+ item.get("fileName"): item for item in parsed if isinstance(item, dict)
118
+ }
119
+
120
+ for fn in file_names:
121
+ if fn in processed_map:
122
+ res = processed_map[fn]
123
+
124
+ # Ensure all required keys exist to prevent Android
125
+ # serialization errors
126
+ res.setdefault("vulnerabilities", [])
127
+ res.setdefault("improvement_suggestions", [])
128
+ res.setdefault("metrics", {"complexity": 5, "maintainability": 5})
129
+
130
+ # Ensure every vulnerability has a suggestion field
131
+ for vuln in res["vulnerabilities"]:
132
+ if "suggestion" not in vuln:
133
+ vuln["suggestion"] = (
134
+ "Review the implementation logic for improved safety."
135
+ )
136
+
137
+ batch_results.append(res)
138
+ else:
139
+ # Fallback if the AI skipped a file in its response
140
+ batch_results.append(
141
+ {
142
+ "fileName": fn,
143
+ "vulnerabilities": [],
144
+ "improvement_suggestions": [
145
+ "No immediate improvements identified."
146
+ ],
147
+ "metrics": {"complexity": 1, "maintainability": 10},
148
+ }
149
+ )
150
+
151
+ except Exception as e:
152
+ logger.error(f"Batch processing error: {e}")
153
+ error_str = str(e)
154
+ is_config_error = any(
155
+ x in error_str
156
+ for x in [
157
+ "QUOTA_EXCEEDED",
158
+ "RATE_LIMIT_EXCEEDED",
159
+ "MISSING_API_KEY",
160
+ "GENERATION_FAILED",
161
+ ]
162
+ )
163
+ for fn in file_names:
164
+ batch_results.append(
165
+ {
166
+ "fileName": fn,
167
+ "vulnerabilities": [],
168
+ "improvement_suggestions": [],
169
  "metrics": {"complexity": 0, "maintainability": 0},
170
+ "error": error_str if is_config_error else "analysis_failed",
171
+ }
172
+ )
173
 
174
+ return batch_results
175
 
176
  def _minify_code(self, code: str, filename: str) -> str:
177
  """
178
  Removes comments, empty lines, and logs to optimize token usage.
179
  """
180
+ lines = code.split("\n")
181
  cleaned_lines = []
182
+
183
+ is_python = filename.endswith(".py")
184
+ is_js_style = filename.endswith(
185
+ (".js", ".ts", ".jsx", ".tsx", ".java", ".kt", ".c", ".cpp")
186
+ )
187
+
188
  for line in lines:
189
  stripped = line.strip()
190
+
191
+ if not stripped:
192
+ continue
193
+
194
  # Skip comments
195
+ if is_python and stripped.startswith("#"):
196
+ continue
197
+ if is_js_style and stripped.startswith(("//", "/*", "*")):
198
+ continue
199
+
200
  # Skip common logging
201
+ if any(
202
+ log in stripped
203
+ for log in ["console.log", "print(", "logger.", "Log.d", "Log.e"]
204
+ ):
205
  continue
206
+
207
  # Strip inline comments
208
+ if is_python and " #" in line:
209
+ line = line.split(" #", 1)[0]
210
+ if is_js_style and " //" in line:
211
+ line = line.split(" //", 1)[0]
212
+
213
+ if not line.strip():
214
+ continue
215
 
216
  cleaned_lines.append(line.rstrip())
217
+
218
+ return "\n".join(cleaned_lines)
219
 
220
  def _parse_json(self, text: str):
221
  """
 
224
  text = text.strip()
225
  if not text:
226
  return []
227
+
228
  # Clean markdown formatting if present
229
+ text = re.sub(r"^[^[]*\[", "[", text)
230
+ text = re.sub(r"\][^]]*$", "]", text)
231
  text = text.replace("```json", "").replace("```", "").strip()
232
+
233
  try:
234
  return json.loads(text)
235
  except json.JSONDecodeError as e:
236
  logger.warning(f"JSON Decode Error: {e}")
237
  return []
238
 
239
+
240
  # Instantiate the service
241
  service = AIReviewerService()