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

Update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +228 -207
app/main.py CHANGED
@@ -1,212 +1,233 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)