diff --git a/src/agents.py b/src/agents.py index 4934e47..da4d58a 100644 --- a/src/agents.py +++ b/src/agents.py @@ -43,16 +43,31 @@ "summary_assistant": """You are a helpful GitHub bot that reviews issues and generates appropriate responses. Analyze the issue details carefully and summarize the suggestions and changes made by other agents. """, - "feedback_assistant": """You are a helpful GitHub bot that processes user feedback on previous bot responses. - Analyze the user's feedback carefully and suggest improvements to the original response. - Focus on addressing specific concerns raised by the user. - Maintain a professional and helpful tone. - DO NOT MAKE ANY CHANGES TO THE FILES OR CREATE NEW FILES. Only provide information or suggestions. - If no changes are needed, respond accordingly. - NEVER ask for user input and NEVER expect it. - If possible, suggest concrete code changes or additions that can be made. Be specific about what files and what lines. - Provide code blocks where you can. - Include any relevant code snippets or technical details from the original response that should be preserved. + "feedback_assistant": """You are a helpful GitHub bot that processes user feedback and error messages. + You can handle two types of input: + 1. User feedback on previous bot responses + 2. Error messages from GitHub Actions workflow runs + + For user feedback: + - Analyze feedback carefully and suggest improvements + - Focus on addressing specific concerns + - Maintain a professional tone + - Suggest concrete code changes where possible + - Include relevant code snippets + + For workflow errors: + - Analyze the error messages carefully + - Identify the root cause of the failure + - Suggest specific fixes for the errors + - Provide code examples where helpful + - Include any relevant documentation links + + General guidelines: + - DO NOT MAKE ANY CHANGES TO FILES. Only provide suggestions. + - Be specific about files and lines that need changes + - NEVER ask for user input + - Use code blocks for all code examples + - Keep responses clear and actionable """, "generate_edit_command_assistant": """You are a helpful GitHub bot that synthesizes all discussion in an issue thread to generate a command for a bot to make edits. Analyze the issue details and comments carefully to generate a detailed and well-organized command. diff --git a/src/git_utils.py b/src/git_utils.py index 3903752..453b89b 100644 --- a/src/git_utils.py +++ b/src/git_utils.py @@ -5,6 +5,36 @@ import os import subprocess import git +import requests +import re +from github.PullRequest import PullRequest + +def get_workflow_logs(repo_full_name, run_id, token): + """Fetch logs for a specific GitHub Actions workflow run""" + url = f"https://api.github.com/repos/{repo_full_name}/actions/runs/{run_id}/logs" + headers = {"Authorization": f"token {token}"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.content + else: + raise Exception(f"Failed to fetch logs: {response.status_code}") + +def create_branch_for_fixes(repo, base_branch, new_branch_name): + """Create a new branch based on the existing branch""" + repo.git.checkout(base_branch) + repo.git.checkout('-b', new_branch_name) + + +def clean_response(response: str) -> str: + """Remove any existing signatures or TERMINATE flags from response text""" + # Remove TERMINATE flags + response = re.sub(r'\bTERMINATE\b', '', response, flags=re.IGNORECASE) + + # Remove existing signatures + response = re.sub( + r'\n\n---\n\*This response was automatically generated by blech_bot\*\s*$', '', response) + + return response.strip() from branch_handler import ( get_issue_related_branches, get_current_branch, @@ -345,6 +375,120 @@ def push_changes_with_authentication( return success_bool, error_msg +def get_workflow_run_logs(repo: Repository, run_id: int) -> List[str]: + """ + Fetch and parse logs from a GitHub Actions workflow run + + Args: + repo: The GitHub repository + run_id: ID of the workflow run + + Returns: + List of extracted log lines + + Raises: + RuntimeError: If logs cannot be fetched + ValueError: If GitHub token is missing + PermissionError: If user lacks required permissions + """ + token = os.getenv('GITHUB_TOKEN') + if not token: + raise ValueError("GitHub token not found in environment variables") + + try: + # Get workflow run logs + logs_url = f"https://api.github.com/repos/{repo.full_name}/actions/runs/{run_id}/logs" + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28" + } + response = requests.get(logs_url, headers=headers) + + if response.status_code == 403: + raise PermissionError( + "Insufficient permissions to access workflow logs. " + "Admin rights to the repository are required." + ) + elif response.status_code == 404: + raise RuntimeError( + f"Workflow run with ID {run_id} not found in repository {repo.full_name}" + ) + + response.raise_for_status() + + # Check if response is HTML (error page) instead of logs + content_type = response.headers.get('content-type', '') + if 'text/html' in content_type: + raise RuntimeError( + "Received HTML response instead of logs. " + "This likely indicates an authentication or permission issue." + ) + + return response.text.splitlines() + + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Failed to fetch workflow logs: {str(e)}") + + +def extract_errors_from_logs(log_lines: List[str]) -> List[str]: + """ + Extract error messages from workflow log lines + + Args: + log_lines: List of log lines to parse + + Returns: + List of extracted error messages + """ + error_lines = [] + capture = False + for line in log_lines: + # Start capturing on error indicators + if any(indicator in line for indicator in [ + "Traceback (most recent call last):", + "error:", + "Error:", + "ERROR:", + "FAILED" + ]): + capture = True + error_lines.append(line) + continue + + # Keep capturing until we hit a likely end + if capture: + error_lines.append(line) + if line.strip() == "" or "Process completed" in line: + capture = False + + return error_lines + + +def get_auto_fix_attempt_count(pr: PullRequest) -> int: + """ + Get the number of auto-fix attempts from PR comments + + Args: + pr: The GitHub pull request + + Returns: + Number of auto-fix attempts made so far + """ + comments = list(pr.get_issue_comments()) + attempt_count = 0 + + for comment in comments: + if "generated by blech_bot" in comment.body and "Attempt " in comment.body: + # Extract attempt number using regex + match = re.search(r"Attempt (\d+)/", comment.body) + if match: + current_attempt = int(match.group(1)) + attempt_count = max(attempt_count, current_attempt) + + return attempt_count + + def has_linked_pr(issue: Issue) -> bool: """ Check if an issue has a linked pull request @@ -368,6 +512,120 @@ def has_linked_pr(issue: Issue) -> bool: return False +def get_workflow_run_logs(repo: Repository, run_id: int) -> List[str]: + """ + Fetch and parse logs from a GitHub Actions workflow run + + Args: + repo: The GitHub repository + run_id: ID of the workflow run + + Returns: + List of extracted log lines + + Raises: + RuntimeError: If logs cannot be fetched + ValueError: If GitHub token is missing + PermissionError: If user lacks required permissions + """ + token = os.getenv('GITHUB_TOKEN') + if not token: + raise ValueError("GitHub token not found in environment variables") + + try: + # Get workflow run logs + logs_url = f"https://api.github.com/repos/{repo.full_name}/actions/runs/{run_id}/logs" + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28" + } + response = requests.get(logs_url, headers=headers) + + if response.status_code == 403: + raise PermissionError( + "Insufficient permissions to access workflow logs. " + "Admin rights to the repository are required." + ) + elif response.status_code == 404: + raise RuntimeError( + f"Workflow run with ID {run_id} not found in repository {repo.full_name}" + ) + + response.raise_for_status() + + # Check if response is HTML (error page) instead of logs + content_type = response.headers.get('content-type', '') + if 'text/html' in content_type: + raise RuntimeError( + "Received HTML response instead of logs. " + "This likely indicates an authentication or permission issue." + ) + + return response.text.splitlines() + + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Failed to fetch workflow logs: {str(e)}") + + +def extract_errors_from_logs(log_lines: List[str]) -> List[str]: + """ + Extract error messages from workflow log lines + + Args: + log_lines: List of log lines to parse + + Returns: + List of extracted error messages + """ + error_lines = [] + capture = False + for line in log_lines: + # Start capturing on error indicators + if any(indicator in line for indicator in [ + "Traceback (most recent call last):", + "error:", + "Error:", + "ERROR:", + "FAILED" + ]): + capture = True + error_lines.append(line) + continue + + # Keep capturing until we hit a likely end + if capture: + error_lines.append(line) + if line.strip() == "" or "Process completed" in line: + capture = False + + return error_lines + + +def get_auto_fix_attempt_count(pr: PullRequest) -> int: + """ + Get the number of auto-fix attempts from PR comments + + Args: + pr: The GitHub pull request + + Returns: + Number of auto-fix attempts made so far + """ + comments = list(pr.get_issue_comments()) + attempt_count = 0 + + for comment in comments: + if "generated by blech_bot" in comment.body and "Attempt " in comment.body: + # Extract attempt number using regex + match = re.search(r"Attempt (\d+)/", comment.body) + if match: + current_attempt = int(match.group(1)) + attempt_count = max(attempt_count, current_attempt) + + return attempt_count + + if __name__ == '__main__': client = get_github_client() repo = get_repository(client, 'katzlabbrandeis/blech_clust') diff --git a/src/response_agent.py b/src/response_agent.py index 2c6f1b2..92ef3a4 100644 --- a/src/response_agent.py +++ b/src/response_agent.py @@ -1,10 +1,18 @@ """ Agent for generating responses to GitHub issues using pyautogen """ -from typing import Optional, Tuple +from typing import Optional, Tuple, List + +# Maximum number of consecutive attempts to auto-fix +MAX_CONSECUTIVE_ATTEMPTS = 5 + +# Maximum number of consecutive attempts to auto-fix +MAX_CONSECUTIVE_ATTEMPTS = 5 from dotenv import load_dotenv import string +import requests +from github.PullRequest import PullRequest import triggers from agents import ( create_user_agent, @@ -16,6 +24,8 @@ import bot_tools from git_utils import ( + get_workflow_logs, + create_branch_for_fixes, get_github_client, get_repository, write_issue_response, @@ -27,6 +37,10 @@ get_development_branch, has_linked_pr, push_changes_with_authentication, + get_auto_fix_attempt_count, + get_workflow_run_logs, + extract_errors_from_logs, + clean_response, ) from github.Repository import Repository from github.Issue import Issue @@ -74,6 +88,14 @@ def check_not_empty(data: str) -> bool: return False +def extract_errors_from_logs(log_content): + """Parse the fetched logs and extract error messages""" + errors = [] + for line in log_content.splitlines(): + if "error" in line.lower(): + errors.append(line) + return errors + def generate_feedback_response( issue: Issue, repo_name: str, @@ -564,6 +586,182 @@ def run_aider(message: str, repo_path: str) -> str: raise RuntimeError(f"Failed to run aider: {e.stderr}") +def process_workflow_errors( + issue: Issue, + repo: Repository, + run_id: int, + pr: PullRequest +) -> None: + """ + Process errors from a GitHub Actions workflow run and post feedback + + Args: + issue: Related GitHub issue + repo: GitHub repository + run_id: Workflow run ID + pr: Related pull request + """ + try: + # Get and parse workflow logs + log_lines = get_workflow_run_logs(repo, run_id) + errors = extract_errors_from_logs(log_lines) + + if errors: + # Track number of attempts to auto-fix + attempts = 0 + + # Create feedback agent + feedback_assistant = create_agent("feedback_assistant", llm_config) + + # Generate prompt with error context + error_text = "\n".join(errors) + + # Initial feedback prompt + feedback_prompt = generate_prompt( + "feedback_assistant", + repo_name=repo.full_name, + repo_path=bot_tools.get_local_repo_path(repo.full_name), + details=get_issue_details(issue), + issue=issue, + original_response="", + feedback_text=f"GitHub Actions workflow failed with errors:\n{error_text}" + ) + + # Get feedback on errors + user_agent = create_user_agent() + feedback_results = user_agent.initiate_chats( + [ + { + "recipient": feedback_assistant, + "message": feedback_prompt, + "max_turns": 10, + "summary_method": "reflection_with_llm", + } + ] + ) + + # Extract solution from feedback + for chat in feedback_results[0].chat_history[::-1]: + if check_not_empty(chat['content']): + solution = chat['content'] + break + + # Get current attempt count + current_attempt = get_auto_fix_attempt_count(pr) + 1 + + # Check if we've reached the maximum attempts + if current_attempt > MAX_CONSECUTIVE_ATTEMPTS: + response = f"Maximum number of auto-fix attempts ({MAX_CONSECUTIVE_ATTEMPTS}) reached.\n\n" + response += f"Please review the errors manually:\n```\n{error_text}\n```" + + write_str = clean_response(response) + signature = "\n\n---\n*This response was automatically generated by blech_bot*" + pr.create_issue_comment(write_str + signature) + return + + # Post feedback as PR comment + response = f"Analysis of workflow failure:\n\n" + response += f"Errors found:\n```\n{error_text}\n```\n\n" + response += f"Suggested solutions:\n{solution}" + response += f"\n\nAttempt {current_attempt}/{MAX_CONSECUTIVE_ATTEMPTS}" + + write_str = clean_response(response) + signature = "\n\n---\n*This response was automatically generated by blech_bot*" + pr.create_issue_comment(write_str + signature) + + except Exception as e: + error_msg = f"Failed to process workflow errors: {str(e)}" + pr.create_issue_comment(error_msg) + + +def process_workflow_errors( + issue: Issue, + repo: Repository, + run_id: int, + pr: PullRequest +) -> None: + """ + Process errors from a GitHub Actions workflow run and post feedback + + Args: + issue: Related GitHub issue + repo: GitHub repository + run_id: Workflow run ID + pr: Related pull request + """ + try: + # Get and parse workflow logs + log_lines = get_workflow_run_logs(repo, run_id) + errors = extract_errors_from_logs(log_lines) + + if errors: + # Track number of attempts to auto-fix + attempts = 0 + + # Create feedback agent + feedback_assistant = create_agent("feedback_assistant", llm_config) + + # Generate prompt with error context + error_text = "\n".join(errors) + + # Initial feedback prompt + feedback_prompt = generate_prompt( + "feedback_assistant", + repo_name=repo.full_name, + repo_path=bot_tools.get_local_repo_path(repo.full_name), + details=get_issue_details(issue), + issue=issue, + original_response="", + feedback_text=f"GitHub Actions workflow failed with errors:\n{error_text}" + ) + + # Get feedback on errors + user_agent = create_user_agent() + feedback_results = user_agent.initiate_chats( + [ + { + "recipient": feedback_assistant, + "message": feedback_prompt, + "max_turns": 10, + "summary_method": "reflection_with_llm", + } + ] + ) + + # Extract solution from feedback + for chat in feedback_results[0].chat_history[::-1]: + if check_not_empty(chat['content']): + solution = chat['content'] + break + + # Get current attempt count + current_attempt = get_auto_fix_attempt_count(pr) + 1 + + # Check if we've reached the maximum attempts + if current_attempt > MAX_CONSECUTIVE_ATTEMPTS: + response = f"Maximum number of auto-fix attempts ({MAX_CONSECUTIVE_ATTEMPTS}) reached.\n\n" + response += f"Please review the errors manually:\n```\n{error_text}\n```" + + write_str = clean_response(response) + signature = "\n\n---\n*This response was automatically generated by blech_bot*" + pr.create_issue_comment(write_str + signature) + return + + # Post feedback as PR comment + response = f"Analysis of workflow failure:\n\n" + response += f"Errors found:\n```\n{error_text}\n```\n\n" + response += f"Suggested solutions:\n{solution}" + response += f"\n\nAttempt {current_attempt}/{MAX_CONSECUTIVE_ATTEMPTS}" + + write_str = clean_response(response) + signature = "\n\n---\n*This response was automatically generated by blech_bot*" + pr.create_issue_comment(write_str + signature) + + except Exception as e: + error_msg = f"Failed to process workflow errors: {str(e)}" + pr.create_issue_comment(error_msg) + + def process_repository( repo_name: str, ) -> None: