diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index b064048c..2ab13d69 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -37,7 +37,7 @@ class PRAgent: def __init__(self): pass - async def handle_request(self, pr_url, request) -> bool: + async def handle_request(self, pr_url, request, notify=None) -> bool: # First, apply repo specific settings if exists if get_settings().config.use_repo_settings_file: repo_settings_file = None @@ -67,8 +67,12 @@ class PRAgent: if action == "reflect_and_review" and not get_settings().pr_reviewer.ask_and_reflect: action = "review" if action == "answer": + if notify: + notify() await PRReviewer(pr_url, is_answer=True, args=args).run() elif action in command2class: + if notify: + notify() await command2class[action](pr_url, args=args).run() else: return False diff --git a/pr_agent/algo/ai_handler.py b/pr_agent/algo/ai_handler.py index 5fcb13c7..fb5f64fe 100644 --- a/pr_agent/algo/ai_handler.py +++ b/pr_agent/algo/ai_handler.py @@ -1,13 +1,15 @@ import logging +import litellm import openai +from litellm import acompletion from openai.error import APIError, RateLimitError, Timeout, TryAgain from retry import retry -import litellm -from litellm import acompletion + from pr_agent.config_loader import get_settings -import traceback -OPENAI_RETRIES=5 + +OPENAI_RETRIES = 5 + class AiHandler: """ @@ -81,15 +83,16 @@ class AiHandler: f"{(' from deployment ' + deployment_id) if deployment_id else ''}" ) response = await acompletion( - model=model, - deployment_id=deployment_id, - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": user} - ], - temperature=temperature, - azure=self.azure - ) + model=model, + deployment_id=deployment_id, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user} + ], + temperature=temperature, + azure=self.azure, + force_timeout=get_settings().config.ai_timeout + ) except (APIError, Timeout, TryAgain) as e: logging.error("Error during OpenAI inference: ", e) raise @@ -104,4 +107,4 @@ class AiHandler: resp = response["choices"][0]['message']['content'] finish_reason = response["choices"][0]["finish_reason"] print(resp, finish_reason) - return resp, finish_reason \ No newline at end of file + return resp, finish_reason diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index 2f3ec2c2..122b0db3 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -89,6 +89,12 @@ class BitbucketProvider: def get_issue_comments(self): raise NotImplementedError("Bitbucket provider does not support issue comments yet") + def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: + return True + + def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: + return True + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 677c2eb1..8e161252 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -3,6 +3,7 @@ from dataclasses import dataclass # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) from enum import Enum +from typing import Optional class EDIT_TYPE(Enum): @@ -88,6 +89,13 @@ class GitProvider(ABC): def get_issue_comments(self): pass + @abstractmethod + def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: + pass + + @abstractmethod + def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: + pass def get_main_pr_language(languages, files) -> str: """ diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index f3018e86..f83216ef 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -2,10 +2,10 @@ import logging import hashlib from datetime import datetime -from typing import Optional, Tuple +from typing import Optional, Tuple, Any from urllib.parse import urlparse -from github import AppAuthentication, Auth, Github, GithubException +from github import AppAuthentication, Auth, Github, GithubException, Reaction from retry import retry from starlette_context import context @@ -153,7 +153,7 @@ class GithubProvider(GitProvider): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file) + position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file) if position == -1: if get_settings().config.verbosity_level >= 2: logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") @@ -263,6 +263,23 @@ class GithubProvider(GitProvider): except Exception: return "" + def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: + try: + reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes") + return reaction.id + except Exception as e: + logging.exception(f"Failed to add eyes reaction, error: {e}") + return None + + def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: + try: + self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id) + return True + except Exception as e: + logging.exception(f"Failed to remove eyes reaction, error: {e}") + return False + + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, int]: parsed_url = urlparse(pr_url) @@ -393,4 +410,4 @@ class GithubProvider(GitProvider): if get_settings().config.verbosity_level >= 2: logging.info(f"Failed adding line link, error: {e}") - return "" \ No newline at end of file + return "" diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 14d1d883..a4d2d127 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -287,6 +287,12 @@ class GitLabProvider(GitProvider): except Exception: return "" + def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: + return True + + def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: + return True + def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[str, int]: parsed_url = urlparse(merge_request_url) diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 9846e199..fbf4f89c 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -4,6 +4,7 @@ import os from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings +from pr_agent.git_providers import get_git_provider from pr_agent.tools.pr_reviewer import PRReviewer @@ -14,6 +15,8 @@ async def run_action(): OPENAI_KEY = os.environ.get('OPENAI_KEY') OPENAI_ORG = os.environ.get('OPENAI_ORG') GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') + get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) + # Check if required environment variables are set if not GITHUB_EVENT_NAME: @@ -61,7 +64,9 @@ async def run_action(): pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url") if pr_url: body = comment_body.strip().lower() - await PRAgent().handle_request(pr_url, body) + comment_id = event_payload.get("comment", {}).get("id") + provider = get_git_provider()(pr_url=pr_url) + await PRAgent().handle_request(pr_url, body, notify=lambda: provider.add_eyes_reaction(comment_id)) if __name__ == '__main__': diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 263f5ba5..18943ae8 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -11,6 +11,7 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings, global_settings +from pr_agent.git_providers import get_git_provider from pr_agent.servers.utils import verify_signature logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) @@ -80,7 +81,10 @@ async def handle_request(body: Dict[str, Any]): return {} pull_request = body["issue"]["pull_request"] api_url = pull_request.get("url") - await agent.handle_request(api_url, comment_body) + comment_id = body.get("comment", {}).get("id") + provider = get_git_provider()(pr_url=api_url) + await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id)) + elif action == "opened" or 'reopened' in action: pull_request = body.get("pull_request") @@ -102,6 +106,7 @@ async def root(): def start(): # Override the deployment type to app get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app") + get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) middleware = [Middleware(RawContextMiddleware)] app = FastAPI(middleware=middleware) app.include_router(router) diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index 18f71dd7..fdd6642d 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -36,6 +36,7 @@ async def polling_loop(): git_provider = get_git_provider()() user_id = git_provider.get_user_id() agent = PRAgent() + get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) try: deployment_type = get_settings().github.deployment_type @@ -98,8 +99,10 @@ async def polling_loop(): if user_tag not in comment_body: continue rest_of_comment = comment_body.split(user_tag)[1].strip() - - success = await agent.handle_request(pr_url, rest_of_comment) + comment_id = comment['id'] + git_provider.set_pr(pr_url) + success = await agent.handle_request(pr_url, rest_of_comment, + notify=lambda: git_provider.add_eyes_reaction(comment_id)) # noqa E501 if not success: git_provider.set_pr(pr_url) git_provider.publish_comment("### How to use PR-Agent\n" + diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index cb9af775..8334049d 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -7,6 +7,7 @@ publish_output_progress=true verbosity_level=0 # 0,1,2 use_extra_bad_extensions=false use_repo_settings_file=true +ai_timeout=180 [pr_reviewer] # /review # require_focused_review=true