CodeCommunity commited on
Commit
5b7d105
·
verified ·
1 Parent(s): 40b9183

Update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +117 -3
app/main.py CHANGED
@@ -4,7 +4,7 @@ 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
@@ -12,8 +12,11 @@ from pydantic import BaseModel
12
  import uvicorn
13
 
14
  load_dotenv()
 
15
  from app.predictor import classifier, guide_generator, reviewer
16
  from app.core.model_loader import llm_engine
 
 
17
 
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
@@ -23,33 +26,60 @@ app = FastAPI(title="GitGud AI Service")
23
  REPO_CACHE: Dict[str, Dict[str, List[float]]] = {}
24
  executor = ThreadPoolExecutor(max_workers=10)
25
 
 
 
 
 
 
26
  class FileRequest(BaseModel):
27
  fileName: str
28
  content: Optional[str] = None
29
  repoName: Optional[str] = None
30
 
 
31
  class BatchReviewRequest(BaseModel):
32
  files: List[FileRequest]
33
 
 
34
  class GuideRequest(BaseModel):
35
  repoName: str
36
  filePaths: List[str]
37
 
 
38
  class SearchRequest(BaseModel):
39
  query: str
40
  embeddings: Optional[Dict[str, List[float]]] = None
41
  repoName: Optional[str] = None
42
 
 
43
  class ChatRequest(BaseModel):
44
  query: str
45
  context: List[Dict[str, str]]
46
  repoName: str
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def calculate_repo_health(total_vulns: int, avg_maint: float) -> int:
49
  base_score = avg_maint * 10.0
50
  penalty = total_vulns * 8.0
51
  return int(max(10.0, min(100.0, base_score - penalty)))
52
 
 
53
  def sync_review_worker(file_list: List[FileRequest]):
54
  logger.info(f"--- [DEBUG] Processing {len(file_list)} files for code review ---")
55
  try:
@@ -59,15 +89,14 @@ def sync_review_worker(file_list: List[FileRequest]):
59
  logger.error(f"--- [ERROR] Exception during review processing: {e} ---", exc_info=True)
60
  raise e
61
 
 
62
  def parse_tree_to_list(raw_tree: str):
63
  nodes = []
64
  for line in raw_tree.strip().split('\n'):
65
  if line.startswith("```") or not line.strip():
66
  continue
67
  level = line.count('|') + (line.count(' ') // 2)
68
- # Strip tree connectors
69
  name = re.sub(r'[|└├─]', '', line).strip()
70
- # Strip AI layer annotations like [Backend] or [Frontend]
71
  name = re.sub(r'\[.*?\]', '', name).strip()
72
  if name:
73
  nodes.append({
@@ -77,6 +106,11 @@ def parse_tree_to_list(raw_tree: str):
77
  })
78
  return nodes
79
 
 
 
 
 
 
80
  @app.get("/")
81
  def health_check():
82
  return {
@@ -86,10 +120,12 @@ def health_check():
86
  "cached_repos": list(REPO_CACHE.keys()),
87
  }
88
 
 
89
  @app.get("/usage")
90
  def get_usage():
91
  return llm_engine.get_usage_stats()
92
 
 
93
  @app.post("/classify")
94
  async def classify_file(request: FileRequest):
95
  try:
@@ -108,6 +144,7 @@ async def classify_file(request: FileRequest):
108
  logger.error(f"Classify failed: {e}")
109
  raise HTTPException(status_code=500, detail=str(e))
110
 
 
111
  @app.post("/review-batch-code")
112
  async def review_batch_code(request: BatchReviewRequest):
113
  try:
@@ -119,6 +156,7 @@ async def review_batch_code(request: BatchReviewRequest):
119
  logger.error(f"Batch review critical failure: {traceback.format_exc()}")
120
  raise HTTPException(status_code=500, detail="Internal processing error")
121
 
 
122
  @app.post("/repo-dashboard-stats")
123
  async def get_dashboard_stats(request: BatchReviewRequest):
124
  try:
@@ -126,23 +164,28 @@ async def get_dashboard_stats(request: BatchReviewRequest):
126
  raw_reviews = await loop.run_in_executor(executor, sync_review_worker, request.files)
127
  if not isinstance(raw_reviews, list):
128
  raw_reviews = [raw_reviews]
 
129
  total_vulns = 0
130
  maint_scores = []
131
  found_apis = set()
132
  api_regex = re.compile(r'(?:get|post|put|delete|patch)\([\'"]\/(.*?)[\'"]', re.IGNORECASE)
 
133
  for i, current_review in enumerate(raw_reviews):
134
  vulns = current_review.get("vulnerabilities", [])
135
  total_vulns += len(vulns)
136
  m_score = current_review.get("metrics", {}).get("maintainability", 8.0)
137
  maint_scores.append(m_score)
 
138
  content = request.files[i].content if i < len(request.files) else None
139
  if content:
140
  matches = api_regex.findall(content)
141
  for match in matches:
142
  found_apis.add(f"/{match}")
 
143
  num_files = len(maint_scores)
144
  avg_maint = (sum(maint_scores) / num_files) if num_files > 0 else 0.0
145
  health_score = calculate_repo_health(total_vulns, avg_maint)
 
146
  return {
147
  "repo_health": health_score,
148
  "health_label": "Excellent" if health_score > 85 else "Good" if health_score > 60 else "Critical",
@@ -156,6 +199,7 @@ async def get_dashboard_stats(request: BatchReviewRequest):
156
  logger.error(f"Stats failed: {e}")
157
  raise HTTPException(status_code=500, detail="Failed to aggregate metrics")
158
 
 
159
  @app.post("/analyze-file")
160
  async def analyze_file(request: FileRequest):
161
  try:
@@ -174,6 +218,7 @@ async def analyze_file(request: FileRequest):
174
  raise HTTPException(status_code=429, detail="Limit Reached")
175
  raise HTTPException(status_code=500, detail=str(e))
176
 
 
177
  @app.post("/semantic-search")
178
  async def semantic_search(request: SearchRequest):
179
  try:
@@ -187,16 +232,19 @@ async def semantic_search(request: SearchRequest):
187
  except Exception as e:
188
  raise HTTPException(status_code=500, detail=str(e))
189
 
 
190
  @app.post("/chat")
191
  async def chat(request: ChatRequest):
192
  try:
193
  context_str = ""
194
  for item in request.context:
195
  context_str += f"--- FILE: {item['fileName']} ---\n{item['content']}\n\n"
 
196
  prompt = f"""You are "GitGud AI", an expert software architect.
197
  Repository: "{request.repoName}"
198
  CONTEXT: {context_str if request.context else "(NO CODE PROVIDED)"}
199
  USER QUESTION: {request.query}"""
 
200
  response = llm_engine.generate_text(prompt)
201
  return {"response": response, "status": "success"}
202
  except Exception as e:
@@ -204,6 +252,7 @@ USER QUESTION: {request.query}"""
204
  return {"response": "⚠️ Daily limit reached. Try again in a bit!", "status": "quota_error"}
205
  raise HTTPException(status_code=500, detail=str(e))
206
 
 
207
  @app.post("/generate-guide")
208
  async def generate_guide(request: GuideRequest):
209
  try:
@@ -222,6 +271,71 @@ async def generate_guide(request: GuideRequest):
222
  raise HTTPException(status_code=429, detail="AI Quota Exceeded")
223
  raise HTTPException(status_code=500, detail=str(e))
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  if __name__ == "__main__":
226
  port = int(os.environ.get("PORT", 7860))
227
  uvicorn.run(app, host="0.0.0.0", port=port)
 
4
  import traceback
5
  import time
6
  import asyncio
7
+ from typing import List, Optional, Dict, Any
8
  from concurrent.futures import ThreadPoolExecutor
9
  from dotenv import load_dotenv
10
  from fastapi import FastAPI, HTTPException, status
 
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
+ from app.services.danger_zone_service import DangerZoneService
19
+ from app.services.convention_service import convention_service
20
 
21
  logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger(__name__)
 
26
  REPO_CACHE: Dict[str, Dict[str, List[float]]] = {}
27
  executor = ThreadPoolExecutor(max_workers=10)
28
 
29
+
30
+ # ──────────────────────────────────────────────
31
+ # Pydantic Models
32
+ # ──────────────────────────────────────────────
33
+
34
  class FileRequest(BaseModel):
35
  fileName: str
36
  content: Optional[str] = None
37
  repoName: Optional[str] = None
38
 
39
+
40
  class BatchReviewRequest(BaseModel):
41
  files: List[FileRequest]
42
 
43
+
44
  class GuideRequest(BaseModel):
45
  repoName: str
46
  filePaths: List[str]
47
 
48
+
49
  class SearchRequest(BaseModel):
50
  query: str
51
  embeddings: Optional[Dict[str, List[float]]] = None
52
  repoName: Optional[str] = None
53
 
54
+
55
  class ChatRequest(BaseModel):
56
  query: str
57
  context: List[Dict[str, str]]
58
  repoName: str
59
 
60
+
61
+ class DestructiveActionRequest(BaseModel):
62
+ repoOwner: str
63
+ repoName: str
64
+ action: str # "hard_reset" | "force_push"
65
+ targetRef: str # e.g. "origin/main" or a SHA
66
+ currentLocalCommits: Optional[List[str]] = None
67
+
68
+
69
+ class ReviewRequest(BaseModel):
70
+ files: List[FileRequest]
71
+
72
+
73
+ # ──────────────────────────────────────────────
74
+ # Helpers
75
+ # ──────────────────────────────────────────────
76
+
77
  def calculate_repo_health(total_vulns: int, avg_maint: float) -> int:
78
  base_score = avg_maint * 10.0
79
  penalty = total_vulns * 8.0
80
  return int(max(10.0, min(100.0, base_score - penalty)))
81
 
82
+
83
  def sync_review_worker(file_list: List[FileRequest]):
84
  logger.info(f"--- [DEBUG] Processing {len(file_list)} files for code review ---")
85
  try:
 
89
  logger.error(f"--- [ERROR] Exception during review processing: {e} ---", exc_info=True)
90
  raise e
91
 
92
+
93
  def parse_tree_to_list(raw_tree: str):
94
  nodes = []
95
  for line in raw_tree.strip().split('\n'):
96
  if line.startswith("```") or not line.strip():
97
  continue
98
  level = line.count('|') + (line.count(' ') // 2)
 
99
  name = re.sub(r'[|└├─]', '', line).strip()
 
100
  name = re.sub(r'\[.*?\]', '', name).strip()
101
  if name:
102
  nodes.append({
 
106
  })
107
  return nodes
108
 
109
+
110
+ # ──────────────────────────────────────────────
111
+ # Core Endpoints
112
+ # ──────────────────────────────────────────────
113
+
114
  @app.get("/")
115
  def health_check():
116
  return {
 
120
  "cached_repos": list(REPO_CACHE.keys()),
121
  }
122
 
123
+
124
  @app.get("/usage")
125
  def get_usage():
126
  return llm_engine.get_usage_stats()
127
 
128
+
129
  @app.post("/classify")
130
  async def classify_file(request: FileRequest):
131
  try:
 
144
  logger.error(f"Classify failed: {e}")
145
  raise HTTPException(status_code=500, detail=str(e))
146
 
147
+
148
  @app.post("/review-batch-code")
149
  async def review_batch_code(request: BatchReviewRequest):
150
  try:
 
156
  logger.error(f"Batch review critical failure: {traceback.format_exc()}")
157
  raise HTTPException(status_code=500, detail="Internal processing error")
158
 
159
+
160
  @app.post("/repo-dashboard-stats")
161
  async def get_dashboard_stats(request: BatchReviewRequest):
162
  try:
 
164
  raw_reviews = await loop.run_in_executor(executor, sync_review_worker, request.files)
165
  if not isinstance(raw_reviews, list):
166
  raw_reviews = [raw_reviews]
167
+
168
  total_vulns = 0
169
  maint_scores = []
170
  found_apis = set()
171
  api_regex = re.compile(r'(?:get|post|put|delete|patch)\([\'"]\/(.*?)[\'"]', re.IGNORECASE)
172
+
173
  for i, current_review in enumerate(raw_reviews):
174
  vulns = current_review.get("vulnerabilities", [])
175
  total_vulns += len(vulns)
176
  m_score = current_review.get("metrics", {}).get("maintainability", 8.0)
177
  maint_scores.append(m_score)
178
+
179
  content = request.files[i].content if i < len(request.files) else None
180
  if content:
181
  matches = api_regex.findall(content)
182
  for match in matches:
183
  found_apis.add(f"/{match}")
184
+
185
  num_files = len(maint_scores)
186
  avg_maint = (sum(maint_scores) / num_files) if num_files > 0 else 0.0
187
  health_score = calculate_repo_health(total_vulns, avg_maint)
188
+
189
  return {
190
  "repo_health": health_score,
191
  "health_label": "Excellent" if health_score > 85 else "Good" if health_score > 60 else "Critical",
 
199
  logger.error(f"Stats failed: {e}")
200
  raise HTTPException(status_code=500, detail="Failed to aggregate metrics")
201
 
202
+
203
  @app.post("/analyze-file")
204
  async def analyze_file(request: FileRequest):
205
  try:
 
218
  raise HTTPException(status_code=429, detail="Limit Reached")
219
  raise HTTPException(status_code=500, detail=str(e))
220
 
221
+
222
  @app.post("/semantic-search")
223
  async def semantic_search(request: SearchRequest):
224
  try:
 
232
  except Exception as e:
233
  raise HTTPException(status_code=500, detail=str(e))
234
 
235
+
236
  @app.post("/chat")
237
  async def chat(request: ChatRequest):
238
  try:
239
  context_str = ""
240
  for item in request.context:
241
  context_str += f"--- FILE: {item['fileName']} ---\n{item['content']}\n\n"
242
+
243
  prompt = f"""You are "GitGud AI", an expert software architect.
244
  Repository: "{request.repoName}"
245
  CONTEXT: {context_str if request.context else "(NO CODE PROVIDED)"}
246
  USER QUESTION: {request.query}"""
247
+
248
  response = llm_engine.generate_text(prompt)
249
  return {"response": response, "status": "success"}
250
  except Exception as e:
 
252
  return {"response": "⚠️ Daily limit reached. Try again in a bit!", "status": "quota_error"}
253
  raise HTTPException(status_code=500, detail=str(e))
254
 
255
+
256
  @app.post("/generate-guide")
257
  async def generate_guide(request: GuideRequest):
258
  try:
 
271
  raise HTTPException(status_code=429, detail="AI Quota Exceeded")
272
  raise HTTPException(status_code=500, detail=str(e))
273
 
274
+
275
+ # ──────────────────────────────────────────────
276
+ # Sandbox / Danger Zone Endpoints (new)
277
+ # ──────────────────────────────────────────────
278
+
279
+ @app.post("/sandbox/simulate-destructive-action")
280
+ async def simulate_danger(request: DestructiveActionRequest):
281
+ try:
282
+ result = DangerZoneService.simulate_destructive_action(
283
+ owner=request.repoOwner,
284
+ repo_name=request.repoName,
285
+ action=request.action,
286
+ target_ref=request.targetRef,
287
+ current_local_shas=request.currentLocalCommits or [],
288
+ )
289
+ return result
290
+ except Exception as e:
291
+ logger.error(f"Danger zone simulation failed: {e}", exc_info=True)
292
+ raise HTTPException(status_code=500, detail=str(e))
293
+
294
+
295
+ @app.post("/sandbox/review-practice-pr")
296
+ async def review_practice(request: ReviewRequest):
297
+ """
298
+ Reuses the exact same AIReviewerService that powers /review-batch-code.
299
+ """
300
+ try:
301
+ loop = asyncio.get_running_loop()
302
+ results = await loop.run_in_executor(executor, sync_review_worker, request.files)
303
+ if not isinstance(results, list):
304
+ results = [results]
305
+
306
+ # One-sentence overall verdict
307
+ verdict_prompt = (
308
+ f"Give a single short sentence verdict on this practice PR. "
309
+ f"Be encouraging but honest. Results summary: {str(results)[:600]}"
310
+ )
311
+ try:
312
+ summary = llm_engine.generate(verdict_prompt, max_tokens=60)
313
+ except Exception:
314
+ summary = "Practice review completed."
315
+
316
+ return {
317
+ "results": results,
318
+ "isPractice": True,
319
+ "summary": summary.strip() if isinstance(summary, str) else "Practice review completed."
320
+ }
321
+ except Exception as e:
322
+ logger.error(f"Practice PR review failed: {e}", exc_info=True)
323
+ raise HTTPException(status_code=500, detail="Practice review failed")
324
+
325
+
326
+ @app.get("/sandbox/repo-conventions")
327
+ async def get_repo_conventions(owner: str, repo: str):
328
+ try:
329
+ return await convention_service.get_conventions(owner, repo)
330
+ except Exception as e:
331
+ logger.error(f"Conventions failed: {e}")
332
+ raise HTTPException(status_code=500, detail=str(e))
333
+
334
+
335
+ # ──────────────────────────────────────────────
336
+ # Entry point
337
+ # ──────────────────────────────────────────────
338
+
339
  if __name__ == "__main__":
340
  port = int(os.environ.get("PORT", 7860))
341
  uvicorn.run(app, host="0.0.0.0", port=port)