CodeCommunity commited on
Commit
1783797
·
verified ·
1 Parent(s): 2522c3f

Update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +207 -228
app/main.py CHANGED
@@ -1,233 +1,212 @@
1
- import os
2
- import re
3
  import logging
4
- import traceback
5
- import time
6
- import asyncio
7
- from typing import List, Optional, Dict
8
- from concurrent.futures import ThreadPoolExecutor
9
- from dotenv import load_dotenv
10
- from fastapi import FastAPI, HTTPException, status
11
- from pydantic import BaseModel
12
- import uvicorn
13
-
14
- load_dotenv()
15
-
16
- from app.predictor import classifier, guide_generator, reviewer
17
  from app.core.model_loader import llm_engine
18
 
19
- logging.basicConfig(level=logging.INFO)
20
  logger = logging.getLogger(__name__)
21
-
22
- app = FastAPI(title="GitGud AI Service")
23
-
24
- REPO_CACHE: Dict[str, Dict[str, List[float]]] = {}
25
- executor = ThreadPoolExecutor(max_workers=8)
26
-
27
- class FileRequest(BaseModel):
28
- fileName: str
29
- content: Optional[str] = None
30
- repoName: Optional[str] = None
31
-
32
- class BatchReviewRequest(BaseModel):
33
- files: List[FileRequest]
34
-
35
- class GuideRequest(BaseModel):
36
- repoName: str
37
- filePaths: List[str]
38
-
39
- class SearchRequest(BaseModel):
40
- query: str
41
- embeddings: Optional[Dict[str, List[float]]] = None
42
- repoName: Optional[str] = None
43
-
44
- class ChatRequest(BaseModel):
45
- query: str
46
- context: List[Dict[str, str]]
47
- repoName: str
48
-
49
- def calculate_repo_health(total_vulns: int, avg_maint: float) -> int:
50
- base_score = avg_maint * 10
51
- penalty = total_vulns * 8
52
- return int(max(10, min(100, base_score - penalty)))
53
-
54
- def sync_review_worker(file_list: List[FileRequest]):
55
- logger.info(f"--- [DEBUG] Processing {len(file_list)} files for code review ---")
56
- try:
57
- results = reviewer.service.review_batch_code(file_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return results
59
- except Exception as e:
60
- logger.error(f"--- [ERROR] Exception during review processing: {e} ---", exc_info=True)
61
- raise e
62
-
63
- def parse_tree_to_list(raw_tree: str):
64
- nodes = []
65
- for line in raw_tree.strip().split('\n'):
66
- level = line.count('|') + (line.count(' ') // 2)
67
- name = re.sub(r'[|└├─]', '', line).strip()
68
- if name:
69
- nodes.append({
70
- "name": name,
71
- "type": "file" if '.' in name else "folder",
72
- "level": level
73
- })
74
- return nodes
75
-
76
- @app.get("/")
77
- def health_check():
78
- return {
79
- "status": "online",
80
- "model": "microsoft/codebert-base",
81
- "device": getattr(classifier, "device", "cpu"),
82
- "cached_repos": list(REPO_CACHE.keys()),
83
- }
84
-
85
- @app.get("/usage")
86
- def get_usage():
87
- return llm_engine.get_usage_stats()
88
-
89
- @app.post("/classify")
90
- async def classify_file(request: FileRequest):
91
- try:
92
- result = classifier.predict(request.fileName, request.content)
93
- if request.repoName:
94
- if request.repoName not in REPO_CACHE:
95
- REPO_CACHE[request.repoName] = {}
96
- REPO_CACHE[request.repoName][request.fileName] = result["embedding"]
97
- return {
98
- "fileName": request.fileName,
99
- "layer": result["label"],
100
- "confidence": result["confidence"],
101
- "embedding": result["embedding"]
102
- }
103
- except Exception as e:
104
- logger.error(f"Classify failed: {e}")
105
- raise HTTPException(status_code=500, detail=str(e))
106
-
107
- @app.post("/review-batch-code")
108
- async def review_batch_code(request: BatchReviewRequest):
109
- try:
110
- loop = asyncio.get_running_loop()
111
- raw_results = await loop.run_in_executor(executor, sync_review_worker, request.files)
112
- final_reviews = raw_results if isinstance(raw_results, list) else [raw_results]
113
- return {"results": final_reviews}
114
- except Exception as e:
115
- logger.error(f"Batch review critical failure: {traceback.format_exc()}")
116
- raise HTTPException(status_code=500, detail="Internal processing error")
117
-
118
- @app.post("/repo-dashboard-stats")
119
- async def get_dashboard_stats(request: BatchReviewRequest):
120
- try:
121
- loop = asyncio.get_running_loop()
122
- raw_reviews = await loop.run_in_executor(executor, sync_review_worker, request.files)
123
- if not isinstance(raw_reviews, list):
124
- raw_reviews = [raw_reviews]
125
-
126
- total_vulns = 0
127
- maint_scores = []
128
- found_apis = set()
129
- api_regex = re.compile(r'(?:get|post|put|delete|patch)\([\'"]\/(.*?)[\'"]', re.IGNORECASE)
130
-
131
- for i, current_review in enumerate(raw_reviews):
132
- vulns = current_review.get("vulnerabilities", [])
133
- total_vulns += len(vulns)
134
- m_score = current_review.get("metrics", {}).get("maintainability", 8.0)
135
- maint_scores.append(m_score)
136
-
137
- content = request.files[i].content if i < len(request.files) else None
138
- if content:
139
- matches = api_regex.findall(content)
140
- for match in matches:
141
- found_apis.add(f"/{match}")
142
-
143
- num_files = len(maint_scores)
144
- avg_maint = (sum(maint_scores) / num_files) if num_files > 0 else 0
145
- health_score = calculate_repo_health(total_vulns, avg_maint)
146
-
147
- return {
148
- "repo_health": health_score,
149
- "health_label": "Excellent" if health_score > 85 else "Good" if health_score > 60 else "Critical",
150
- "security_issues": total_vulns,
151
- "performance_ratio": f"{int(avg_maint * 10)}%",
152
- "exposed_apis": list(found_apis),
153
- "total_files_processed": num_files,
154
- "average_maintainability": round(avg_maint, 1)
155
- }
156
- except Exception as e:
157
- logger.error(f"Stats failed: {e}")
158
- raise HTTPException(status_code=500, detail="Failed to aggregate metrics")
159
-
160
- @app.post("/analyze-file")
161
- async def analyze_file(request: FileRequest):
162
- try:
163
- result = classifier.predict(request.fileName, request.content)
164
- summary = classifier.generate_file_summary(request.content, request.fileName)
165
- tags = classifier.extract_tags(request.content, request.fileName)
166
- return {
167
- "fileName": request.fileName,
168
- "layer": result["label"],
169
- "summary": summary,
170
- "tags": tags,
171
- "embedding": result["embedding"],
172
- }
173
- except Exception as e:
174
- if "429" in str(e):
175
- raise HTTPException(status_code=429, detail="Limit Reached")
176
- raise HTTPException(status_code=500, detail=str(e))
177
-
178
- @app.post("/semantic-search")
179
- async def semantic_search(request: SearchRequest):
180
- try:
181
- embeddings = request.embeddings
182
- if not embeddings and request.repoName and request.repoName in REPO_CACHE:
183
- embeddings = REPO_CACHE[request.repoName]
184
-
185
- if not embeddings:
186
- return {"results": []}
187
-
188
- results = classifier.semantic_search(request.query, embeddings)
189
- return {"results": results}
190
- except Exception as e:
191
- raise HTTPException(status_code=500, detail=str(e))
192
-
193
- @app.post("/chat")
194
- async def chat(request: ChatRequest):
195
- try:
196
- context_str = ""
197
- for item in request.context:
198
- context_str += f"--- FILE: {item['fileName']} ---\n{item['content']}\n\n"
199
-
200
- prompt = f"""You are "GitGud AI", an expert software architect.
201
- Repository: "{request.repoName}"
202
- CONTEXT: {context_str if request.context else "(NO CODE PROVIDED)"}
203
- USER QUESTION: {request.query}"""
204
-
205
- response = llm_engine.generate_text(prompt)
206
- return {"response": response, "status": "success"}
207
- except Exception as e:
208
- if "429" in str(e):
209
- return {"response": "⚠️ Daily limit reached. Try again in a bit!", "status": "quota_error"}
210
- raise HTTPException(status_code=500, detail=str(e))
211
-
212
- @app.post("/generate-guide")
213
- async def generate_guide(request: GuideRequest):
214
- try:
215
- markdown = guide_generator.generate_markdown(request.repoName, request.filePaths)
216
- tree_match = re.search(r"Project Structure\n\n(.*?)(?=\n\n|$)", markdown, re.S)
217
- structured_tree = []
218
- if tree_match:
219
- structured_tree = parse_tree_to_list(tree_match.group(1))
220
-
221
- return {
222
- "markdown": markdown,
223
- "structured_tree": structured_tree,
224
- "project_name": request.repoName
225
- }
226
- except Exception as e:
227
- if "429" in str(e):
228
- raise HTTPException(status_code=429, detail="AI Quota Exceeded")
229
- raise HTTPException(status_code=500, detail=str(e))
230
-
231
- if __name__ == "__main__":
232
- port = int(os.environ.get("PORT", 7860))
233
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ import json
 
2
  import logging
3
+ import re
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 (
26
+ file_hash TEXT PRIMARY KEY,
27
+ result_json TEXT
28
+ )
29
+ """)
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()
43
+ if row:
44
+ try:
45
+ return json.loads(row[0])
46
+ except json.JSONDecodeError:
47
+ return None
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 (?, ?)",
58
+ [(h, json.dumps(res)) for h, res in hash_result_pairs]
59
+ )
60
+ conn.commit()
61
+
62
+ def review_batch_code(self, files: list) -> list:
63
+ if not files:
64
+ return []
65
+
66
+ results = [None] * len(files)
67
+ uncached_files = []
68
+ uncached_indices = []
69
+
70
+ # 1. Check SQLite cache
71
+ for idx, f in enumerate(files):
72
+ file_hash = self._generate_file_hash(f)
73
+ cached_res = self._get_cached_result(file_hash)
74
+ if cached_res:
75
+ results[idx] = cached_res
76
+ else:
77
+ uncached_files.append(f)
78
+ uncached_indices.append(idx)
79
+
80
+ if not uncached_files:
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,
113
+ {
114
+ "fileName": f.fileName,
115
+ "vulnerabilities": [],
116
+ "improvement_suggestions": [],
117
+ "metrics": {"complexity": 0, "maintainability": 0},
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)
127
  return results
128
+
129
+ def _process_batch(self, batch: list) -> list:
130
+ combined_code = ""
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:
158
+ res = processed_map[fn]
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()
190
+ if not stripped:
191
+ continue
192
+ if is_python and stripped.startswith("#"):
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()