Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -1,281 +1,283 @@
|
|
1 |
-
import sys
|
2 |
-
import shutil
|
3 |
-
import logging
|
4 |
-
import time
|
5 |
import os
|
6 |
-
|
7 |
-
from
|
8 |
import requests
|
9 |
-
import
|
10 |
-
import
|
11 |
-
import
|
12 |
-
|
13 |
-
import
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
self.headers = {
|
47 |
-
'Authorization': f'token {token}',
|
48 |
-
'Accept': 'application/vnd.github.v3+json'
|
49 |
}
|
50 |
-
self.base_url = "https://api.github.com"
|
51 |
|
52 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
try:
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
if
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
-
def get_repository(self, owner: str, repo: str) -> Dict:
|
72 |
-
try:
|
73 |
-
response = requests.get(f"{self.base_url}/repos/{owner}/{repo}", headers=self.headers)
|
74 |
-
response.raise_for_status()
|
75 |
-
return response.json()
|
76 |
-
except requests.HTTPError as e:
|
77 |
-
self.logger.error(f"HTTP error getting repository info for {owner}/{repo}: {str(e)}. Please check the repository details.")
|
78 |
-
raise
|
79 |
except Exception as e:
|
80 |
-
self.logger.error(f"Error getting
|
81 |
raise
|
82 |
|
83 |
-
def
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
try:
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
#
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
-
def initialize_api(self, token: str):
|
103 |
-
self.github_api = GitHubAPI(token, self.logger)
|
104 |
-
|
105 |
-
def fetch_issues(self, token: str, owner: str, repo: str) -> List[Dict]:
|
106 |
-
try:
|
107 |
-
self.initialize_api(token)
|
108 |
-
return self.github_api.get_issues(owner, repo)
|
109 |
except Exception as e:
|
110 |
-
self.logger.error(f"Error
|
111 |
-
return
|
112 |
|
113 |
-
def
|
|
|
|
|
|
|
114 |
try:
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
#
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
|
|
|
|
|
|
147 |
|
148 |
except Exception as e:
|
149 |
-
|
150 |
-
|
151 |
-
return error_msg
|
152 |
-
|
153 |
-
def suggest_automated_fixes(self, issue_title: str) -> str:
|
154 |
-
if "missing README" in issue_title.lower():
|
155 |
-
return "Consider adding a README.md file to provide project documentation."
|
156 |
-
return "No automated fix available for this issue."
|
157 |
-
|
158 |
-
def handle_issue_selection(token, owner, repo, issue_number, resolution, forked_repo):
|
159 |
-
bot = GitHubBot(logger)
|
160 |
-
result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo)
|
161 |
-
return result
|
162 |
-
|
163 |
-
def extract_info_from_url(url: str) -> Dict[str, Any]:
|
164 |
-
info = {}
|
165 |
-
try:
|
166 |
-
response = requests.get(url)
|
167 |
-
response.raise_for_status()
|
168 |
-
info['status_code'] = response.status_code
|
169 |
-
info['headers'] = dict(response.headers)
|
170 |
-
info['content'] = response.text[:500] # Limit content to first 500 characters for brevity
|
171 |
-
|
172 |
-
parsed_url = urlparse(url)
|
173 |
-
if 'github.com' in parsed_url.netloc:
|
174 |
-
parts = parsed_url.path.split('/')
|
175 |
-
if len(parts) > 2:
|
176 |
-
owner = parts[1]
|
177 |
-
repo = parts[2]
|
178 |
-
issues = bot.fetch_issues(github_token, owner, repo)
|
179 |
-
info['issues'] = issues
|
180 |
-
elif 'huggingface.co' in parsed_url.netloc:
|
181 |
-
# Add Hugging Face specific handling if needed
|
182 |
-
pass
|
183 |
-
|
184 |
-
except requests.HTTPError as e:
|
185 |
-
info['error'] = f"HTTP error: {str(e)}"
|
186 |
-
except Exception as e:
|
187 |
-
info['error'] = f"Error: {str(e)}"
|
188 |
-
return info
|
189 |
-
|
190 |
-
# Initialize GitHubBot globally
|
191 |
-
bot = GitHubBot(logger)
|
192 |
-
|
193 |
-
# Define missing functions with validation
|
194 |
-
def fetch_issues(token, repo_url):
|
195 |
-
try:
|
196 |
-
parts = repo_url.split('/')
|
197 |
-
if len(parts) < 2:
|
198 |
-
raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
|
199 |
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
shutil.rmtree(dir_name)
|
263 |
-
logging.shutdown()
|
264 |
-
except Exception as e:
|
265 |
-
print(f"Error during cleanup: {str(e)}")
|
266 |
|
267 |
-
|
268 |
-
# Initialize environment and logger
|
269 |
-
initialize_environment()
|
270 |
-
global logger
|
271 |
-
logger = initialize_logger()
|
272 |
|
273 |
-
|
274 |
-
atexit.register(cleanup)
|
275 |
|
276 |
-
|
277 |
-
|
278 |
-
|
|
|
|
|
|
|
|
|
279 |
|
280 |
-
if __name__ == "__main__":
|
281 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
+
import github
|
3 |
+
from github import Github
|
4 |
import requests
|
5 |
+
from typing import List, Dict, Optional
|
6 |
+
import logging
|
7 |
+
from datetime import datetime
|
8 |
+
import pytest
|
9 |
+
from abc import ABC, abstractmethod
|
10 |
+
import base64
|
11 |
+
from concurrent.futures import ThreadPoolExecutor
|
12 |
+
import re
|
13 |
+
from dataclasses import dataclass
|
14 |
+
from enum import Enum
|
15 |
+
|
16 |
+
class IssueSeverity(Enum):
|
17 |
+
CRITICAL = 5
|
18 |
+
HIGH = 4
|
19 |
+
MEDIUM = 3
|
20 |
+
LOW = 2
|
21 |
+
TRIVIAL = 1
|
22 |
+
|
23 |
+
@dataclass
|
24 |
+
class CodeContext:
|
25 |
+
file_path: str
|
26 |
+
content: str
|
27 |
+
language: str
|
28 |
+
|
29 |
+
class GitHubGuardianAngel:
|
30 |
+
def __init__(self, github_token: str, ai_provider: AIProvider):
|
31 |
+
self.gh = Github(github_token)
|
32 |
+
self.ai = ai_provider
|
33 |
+
self.logger = self._setup_logging()
|
34 |
+
self.supported_extensions = {
|
35 |
+
'.py': 'Python',
|
36 |
+
'.js': 'JavaScript',
|
37 |
+
'.ts': 'TypeScript',
|
38 |
+
'.java': 'Java',
|
39 |
+
'.cpp': 'C++',
|
40 |
+
'.go': 'Go',
|
41 |
+
'.rs': 'Rust'
|
|
|
|
|
|
|
42 |
}
|
|
|
43 |
|
44 |
+
def _setup_logging(self):
|
45 |
+
logger = logging.getLogger('guardian_angel')
|
46 |
+
logger.setLevel(logging.INFO)
|
47 |
+
handler = logging.FileHandler('guardian_angel.log')
|
48 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
49 |
+
handler.setFormatter(formatter)
|
50 |
+
logger.addHandler(handler)
|
51 |
+
return logger
|
52 |
+
|
53 |
+
def _get_main_branch_content(self, repo) -> List[CodeContext]:
|
54 |
+
"""
|
55 |
+
Retrieves and analyzes the content of the main branch
|
56 |
+
"""
|
57 |
try:
|
58 |
+
# Get default branch
|
59 |
+
default_branch = repo.default_branch
|
60 |
+
branch = repo.get_branch(default_branch)
|
61 |
+
tree = repo.get_git_tree(branch.commit.sha, recursive=True)
|
62 |
+
|
63 |
+
code_contexts = []
|
64 |
+
|
65 |
+
def process_file(element):
|
66 |
+
if element.type == 'blob':
|
67 |
+
_, ext = os.path.splitext(element.path)
|
68 |
+
if ext in self.supported_extensions:
|
69 |
+
try:
|
70 |
+
content = repo.get_contents(element.path).decoded_content.decode('utf-8')
|
71 |
+
return CodeContext(
|
72 |
+
file_path=element.path,
|
73 |
+
content=content,
|
74 |
+
language=self.supported_extensions[ext]
|
75 |
+
)
|
76 |
+
except Exception as e:
|
77 |
+
self.logger.warning(f"Failed to process file {element.path}: {str(e)}")
|
78 |
+
return None
|
79 |
+
|
80 |
+
# Process files in parallel
|
81 |
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
82 |
+
results = list(executor.map(process_file, tree.tree))
|
83 |
+
|
84 |
+
code_contexts = [r for r in results if r is not None]
|
85 |
+
|
86 |
+
return code_contexts
|
87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
except Exception as e:
|
89 |
+
self.logger.error(f"Error getting main branch content: {str(e)}")
|
90 |
raise
|
91 |
|
92 |
+
def _determine_severity(self, issue, codebase_analysis) -> IssueSeverity:
|
93 |
+
"""
|
94 |
+
Determines issue severity based on various factors
|
95 |
+
"""
|
96 |
try:
|
97 |
+
severity_indicators = {
|
98 |
+
'critical': ['crash', 'security', 'vulnerability', 'urgent', 'production down'],
|
99 |
+
'high': ['bug', 'error', 'failure', 'broken'],
|
100 |
+
'medium': ['enhancement', 'improvement', 'update needed'],
|
101 |
+
'low': ['minor', 'cosmetic', 'style', 'documentation'],
|
102 |
+
'trivial': ['typo', 'formatting']
|
103 |
+
}
|
104 |
+
|
105 |
+
# Check labels
|
106 |
+
label_texts = [label.name.lower() for label in issue.labels]
|
107 |
+
|
108 |
+
# Check title and body
|
109 |
+
text_to_analyze = f"{issue.title.lower()} {issue.body.lower()}"
|
110 |
+
|
111 |
+
# Calculate severity score
|
112 |
+
severity_score = 0
|
113 |
+
|
114 |
+
for severity, indicators in severity_indicators.items():
|
115 |
+
for indicator in indicators:
|
116 |
+
if indicator in text_to_analyze or any(indicator in label for label in label_texts):
|
117 |
+
if severity == 'critical':
|
118 |
+
severity_score = max(severity_score, 5)
|
119 |
+
elif severity == 'high':
|
120 |
+
severity_score = max(severity_score, 4)
|
121 |
+
elif severity == 'medium':
|
122 |
+
severity_score = max(severity_score, 3)
|
123 |
+
elif severity == 'low':
|
124 |
+
severity_score = max(severity_score, 2)
|
125 |
+
else:
|
126 |
+
severity_score = max(severity_score, 1)
|
127 |
+
|
128 |
+
# Consider issue age
|
129 |
+
age_days = (datetime.now() - issue.created_at).days
|
130 |
+
if age_days > 30:
|
131 |
+
severity_score += 1
|
132 |
+
if age_days > 90:
|
133 |
+
severity_score += 1
|
134 |
+
|
135 |
+
# Map score to severity enum
|
136 |
+
return IssueSeverity(min(severity_score, 5))
|
137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
except Exception as e:
|
139 |
+
self.logger.error(f"Error determining severity: {str(e)}")
|
140 |
+
return IssueSeverity.MEDIUM
|
141 |
|
142 |
+
def _get_issue_context(self, repo, issue) -> str:
|
143 |
+
"""
|
144 |
+
Gathers relevant context for the issue including related code and discussions
|
145 |
+
"""
|
146 |
try:
|
147 |
+
context_parts = []
|
148 |
+
|
149 |
+
# Add issue details
|
150 |
+
context_parts.append(f"Issue #{issue.number}: {issue.title}")
|
151 |
+
context_parts.append(f"Description: {issue.body}")
|
152 |
+
|
153 |
+
# Add labels
|
154 |
+
context_parts.append(f"Labels: {', '.join([l.name for l in issue.labels])}")
|
155 |
+
|
156 |
+
# Add related files (if mentioned in the issue)
|
157 |
+
file_patterns = re.findall(r'`(.*?)`|\b\w+\.[a-zA-Z]+\b', issue.body)
|
158 |
+
related_files = []
|
159 |
+
|
160 |
+
for pattern in file_patterns:
|
161 |
+
try:
|
162 |
+
content = repo.get_contents(pattern)
|
163 |
+
if isinstance(content, list):
|
164 |
+
continue
|
165 |
+
decoded_content = content.decoded_content.decode('utf-8')
|
166 |
+
related_files.append(f"File: {pattern}\n```\n{decoded_content}\n```")
|
167 |
+
except:
|
168 |
+
continue
|
169 |
+
|
170 |
+
if related_files:
|
171 |
+
context_parts.append("Related Files:")
|
172 |
+
context_parts.extend(related_files)
|
173 |
+
|
174 |
+
# Add comments
|
175 |
+
comments = issue.get_comments()
|
176 |
+
if comments.totalCount > 0:
|
177 |
+
context_parts.append("Relevant Comments:")
|
178 |
+
for comment in comments[:5]: # Limit to last 5 comments
|
179 |
+
context_parts.append(f"Comment by {comment.user.login}:\n{comment.body}")
|
180 |
+
|
181 |
+
return "\n\n".join(context_parts)
|
182 |
|
183 |
except Exception as e:
|
184 |
+
self.logger.error(f"Error getting issue context: {str(e)}")
|
185 |
+
return f"Issue #{issue.number}: {issue.title}\n{issue.body}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
|
187 |
+
def _test_solution(self, solution: str, repo) -> Dict:
|
188 |
+
"""
|
189 |
+
Tests the proposed solution using pytest
|
190 |
+
"""
|
191 |
+
try:
|
192 |
+
# Create temporary test directory
|
193 |
+
test_dir = "temp_test_dir"
|
194 |
+
os.makedirs(test_dir, exist_ok=True)
|
195 |
+
|
196 |
+
# Extract code blocks from solution
|
197 |
+
code_blocks = re.findall(r'```(?:python)?\n(.*?)```', solution, re.DOTALL)
|
198 |
+
|
199 |
+
test_results = {
|
200 |
+
'status': 'untested',
|
201 |
+
'details': [],
|
202 |
+
'error': None
|
203 |
+
}
|
204 |
+
|
205 |
+
if not code_blocks:
|
206 |
+
test_results['status'] = 'no_code_found'
|
207 |
+
return test_results
|
208 |
+
|
209 |
+
# Write code blocks to test files
|
210 |
+
for i, code in enumerate(code_blocks):
|
211 |
+
test_file = os.path.join(test_dir, f'test_solution_{i}.py')
|
212 |
+
with open(test_file, 'w') as f:
|
213 |
+
f.write(code)
|
214 |
+
|
215 |
+
try:
|
216 |
+
# Run pytest on the file
|
217 |
+
test_output = pytest.main(['-v', test_file])
|
218 |
+
test_results['details'].append({
|
219 |
+
'file': f'test_solution_{i}.py',
|
220 |
+
'status': 'passed' if test_output == 0 else 'failed',
|
221 |
+
'output': str(test_output)
|
222 |
+
})
|
223 |
+
except Exception as e:
|
224 |
+
test_results['details'].append({
|
225 |
+
'file': f'test_solution_{i}.py',
|
226 |
+
'status': 'error',
|
227 |
+
'error': str(e)
|
228 |
+
})
|
229 |
+
|
230 |
+
# Determine overall status
|
231 |
+
if any(d['status'] == 'error' for d in test_results['details']):
|
232 |
+
test_results['status'] = 'error'
|
233 |
+
elif any(d['status'] == 'failed' for d in test_results['details']):
|
234 |
+
test_results['status'] = 'failed'
|
235 |
+
else:
|
236 |
+
test_results['status'] = 'passed'
|
237 |
+
|
238 |
+
return test_results
|
239 |
+
|
240 |
+
except Exception as e:
|
241 |
+
self.logger.error(f"Error testing solution: {str(e)}")
|
242 |
+
return {
|
243 |
+
'status': 'error',
|
244 |
+
'details': [],
|
245 |
+
'error': str(e)
|
246 |
+
}
|
247 |
+
finally:
|
248 |
+
# Cleanup
|
249 |
+
if os.path.exists(test_dir):
|
250 |
+
import shutil
|
251 |
+
shutil.rmtree(test_dir)
|
252 |
+
|
253 |
+
def _comment_solution(self, issue, solution: str, test_results: Dict):
|
254 |
+
"""
|
255 |
+
Posts a detailed solution comment on the issue
|
256 |
+
"""
|
257 |
+
status_emoji = {
|
258 |
+
'passed': '✅',
|
259 |
+
'failed': '❌',
|
260 |
+
'error': '⚠️',
|
261 |
+
'untested': '⚪',
|
262 |
+
'no_code_found': '❓'
|
263 |
+
}
|
264 |
|
265 |
+
comment = f"""
|
266 |
+
## 🔮 GitHub Guardian Angel Analysis
|
267 |
+
### Proposed Solution:
|
268 |
+
{solution}
|
269 |
+
Test Results {status_emoji.get(test_results['status'], '⚪')}
|
270 |
+
Status: {test_results['status'].upper()}
|
|
|
|
|
|
|
|
|
271 |
|
272 |
+
{"#### Test Details:" if test_results['details'] else ""} {"".join([f"- {d['file']}: {status_emoji.get(d['status'], '⚪')} {d['status'].upper()}\n" for d in test_results['details']])}
|
|
|
|
|
|
|
|
|
273 |
|
274 |
+
{f"⚠️ Error: {test_results['error']}" if test_results.get('error') else ""}
|
|
|
275 |
|
276 |
+
Implementation Steps:
|
277 |
+
Review the proposed solution and test results
|
278 |
+
Apply the changes in the code blocks above
|
279 |
+
Run the provided tests to verify the fix
|
280 |
+
If tests pass, commit and push the changes
|
281 |
+
Close this issue with a reference to the fixing commit
|
282 |
+
💡 Please provide feedback on this solution. If you need any clarification or adjustments, let me know! """ try: issue.create_comment(comment) self.logger.info(f"Posted solution comment on issue #{issue.number}") except Exception as e: self.logger.error(f"Error posting comment: {str(e)}") raise
|
283 |
|
|
|
|