acecalisto3 commited on
Commit
c6a5c1a
·
verified ·
1 Parent(s): c9f4336

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -259
app.py CHANGED
@@ -1,281 +1,283 @@
1
- import sys
2
- import shutil
3
- import logging
4
- import time
5
  import os
6
- from datetime import datetime
7
- from typing import List, Dict, Any
8
  import requests
9
- import gradio as gr
10
- import atexit
11
- import subprocess
12
- from urllib.parse import urlparse, quote
13
- import webbrowser
14
-
15
- # Constants
16
- INPUT_DIRECTORY = 'input'
17
- OUTPUT_DIRECTORY = 'output'
18
- LOGS_DIRECTORY = 'logs'
19
- RESOLUTIONS_DIRECTORY = 'resolutions'
20
- REPOS_DIRECTORY = 'repos'
21
-
22
- # Set up logging
23
- def initialize_logger() -> logging.Logger:
24
- log_file = f"{LOGS_DIRECTORY}/github_bot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
25
- logging.basicConfig(
26
- level=logging.INFO,
27
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
28
- handlers=[
29
- logging.FileHandler(log_file),
30
- logging.StreamHandler()
31
- ]
32
- )
33
- return logging.getLogger(__name__)
34
-
35
- # Initialize environment and logger
36
- def initialize_environment():
37
- directories = [LOGS_DIRECTORY, RESOLUTIONS_DIRECTORY, REPOS_DIRECTORY, INPUT_DIRECTORY, OUTPUT_DIRECTORY]
38
- for directory in directories:
39
- os.makedirs(directory, exist_ok=True)
40
-
41
- # GitHub API handler
42
- class GitHubAPI:
43
- def __init__(self, token: str, logger: logging.Logger):
44
- self.token = token
45
- self.logger = logger
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 _check_rate_limit(self) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
53
  try:
54
- response = requests.get(f"{self.base_url}/rate_limit", headers=self.headers)
55
- response.raise_for_status()
56
- limits = response.json()
57
- remaining = limits['resources']['core']['remaining']
58
- reset_time = limits['resources']['core']['reset']
59
-
60
- if remaining < 10:
61
- wait_time = max(0, reset_time - int(time.time()))
62
- if wait_time > 0:
63
- self.logger.warning(f"Rate limit nearly exceeded. Waiting {wait_time} seconds before retrying...")
64
- time.sleep(wait_time)
65
- return False
66
- return True
67
- except requests.exceptions.RequestException as e:
68
- self.logger.error(f"Error checking rate limit: {str(e)}. Retrying...")
69
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
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 repository info: {str(e)}")
81
  raise
82
 
83
- def get_issues(self, owner: str, repo: str, state: str = 'open') -> List[Dict]:
84
- if not self._check_rate_limit():
85
- return []
86
-
87
  try:
88
- response = requests.get(f"{self.base_url}/repos/{owner}/{repo}/issues", headers=self.headers, params={'state': state})
89
- response.raise_for_status()
90
- issues = response.json()
91
- return [issue for issue in issues if 'pull_request' not in issue]
92
- except Exception as e:
93
- self.logger.error(f"Error fetching issues for repository {owner}/{repo}: {str(e)}. Please verify the repository and token.")
94
- return []
95
-
96
- # GitHub Bot
97
- class GitHubBot:
98
- def __init__(self, logger: logging.Logger):
99
- self.github_api = None
100
- self.logger = logger
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 fetching issues for repository {owner}/{repo}: {str(e )}")
111
- return []
112
 
113
- def resolve_issue(self, token: str, owner: str, repo: str, issue_number: int, resolution: str, forked_repo: str) -> str:
 
 
 
114
  try:
115
- self.initialize_api(token)
116
- self.github_api.get_repository(owner, repo)
117
-
118
- # Create resolution file
119
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
120
- resolution_file = f"{RESOLUTIONS_DIRECTORY}/resolution_{issue_number}_{timestamp}.md"
121
-
122
- with open(resolution_file, "w") as f:
123
- f.write(f"# Resolution for Issue #{issue_number}\n\n{resolution}")
124
-
125
- # Validate forked_repo before cloning
126
- if not forked_repo:
127
- raise ValueError("Forked repository URL cannot be empty.")
128
-
129
- # Clone the forked repo
130
- subprocess.run(['git', 'clone', forked_repo, '/tmp/' + forked_repo.split('/')[-1]], check=True)
131
-
132
- # Change to the cloned directory
133
- os.chdir('/tmp/' + forked_repo.split('/')[-1])
134
-
135
- # Assuming manual intervention now
136
- input("Apply the fix manually and stage the changes (press ENTER)? ")
137
-
138
- # Commit and push the modifications
139
- subprocess.run(['git', 'add', '.'], check=True)
140
- subprocess.run(['git', 'commit', '-m', f"Resolved issue #{issue_number} ({quote(resolution)})"], check=True)
141
- subprocess.run(['git', 'push', 'origin', 'HEAD'], check=True)
142
-
143
- # Open Pull Request page
144
- webbrowser.open(f'https://github.com/{forked_repo.split("/")[-1]}/compare/master...{owner}:{forked_repo.split("/")[-1]}_resolved_issue_{issue_number}')
145
-
146
- return f"Resolution saved: {resolution_file}"
 
 
 
147
 
148
  except Exception as e:
149
- error_msg = f"Error resolving issue #{issue_number} in repository {owner}/{repo}: {str(e)}"
150
- self.logger.error(error_msg)
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
- owner, repo = parts[-2], parts[-1]
201
- issues = bot.fetch_issues(token, owner, repo)
202
- return issues
203
- except Exception as e:
204
- return str(e)
205
-
206
- def resolve_issue(token, repo_url, issue_number, resolution, forked_repo_url):
207
- try:
208
- parts = repo_url.split('/')
209
- if len(parts) < 2:
210
- raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
211
-
212
- owner, repo = parts[-2], parts[-1]
213
- result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo_url)
214
- return result
215
- except Exception as e:
216
- return str(e)
217
-
218
- def extract_info(url):
219
- try:
220
- info = extract_info_from_url(url)
221
- return info
222
- except Exception as e:
223
- return str(e)
224
-
225
- def create_gradio_interface():
226
- with gr.Blocks() as demo:
227
- gr.Markdown("# GitHub Issue Resolver")
228
- gr.Markdown("This application allows you to fetch and resolve GitHub issues efficiently.")
229
-
230
- with gr.Row():
231
- token_input = gr.Textbox(label="GitHub Token", placeholder="Enter your GitHub token")
232
- repo_url_input = gr.Textbox(label="Repository URL", placeholder="Enter the repository URL (owner/repo)")
233
-
234
- with gr.Row():
235
- issue_number_input = gr.Number(label="Issue Number", info="Enter the issue number")
236
- resolution_input = gr.Textbox(label="Resolution", placeholder="Describe the resolution for the issue", lines=4)
237
-
238
- forked_repo_input = gr.Textbox(label="Forked Repository URL", placeholder="Enter the forked repository URL")
239
-
240
- submit_button = gr.Button("Resolve Issue")
241
- result_output = gr.Textbox(label="Result", interactive=False)
242
-
243
- def on_submit(token, repo_url, issue_number, resolution, forked_repo):
244
- issues = fetch_issues(token, repo_url)
245
- if issues:
246
- automated_fix = bot.suggest_automated_fixes(issues[0]['title'])
247
- resolution += f"\n\n**Automated Suggestion:** {automated_fix}"
248
- result = resolve_issue(token, repo_url, issue_number, resolution, forked_repo)
249
- return result
250
- return "No issues found or an error occurred."
251
-
252
- submit_button.click(on_submit, inputs=[token_input, repo_url_input, issue_number_input, resolution_input, forked_repo_input], outputs=result_output)
253
-
254
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
- # Cleanup function
257
- def cleanup():
258
- try:
259
- temp_dirs = [REPOS_DIRECTORY]
260
- for dir_name in temp_dirs:
261
- if os.path.exists(dir_name):
262
- shutil.rmtree(dir_name)
263
- logging.shutdown()
264
- except Exception as e:
265
- print(f"Error during cleanup: {str(e)}")
266
 
267
- def main():
268
- # Initialize environment and logger
269
- initialize_environment()
270
- global logger
271
- logger = initialize_logger()
272
 
273
- # Register cleanup handlers
274
- atexit.register(cleanup)
275
 
276
- # Create Gradio interface
277
- demo = create_gradio_interface()
278
- demo.launch()
 
 
 
 
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