From 8f751f7371569a503948ec6188b16285e0dc65bb Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 7 Aug 2023 13:26:28 +0300 Subject: [PATCH 1/6] Default timeout for AI is now 180s, configurable --- pr_agent/algo/ai_handler.py | 31 +++++++++++++++------------- pr_agent/settings/configuration.toml | 1 + 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pr_agent/algo/ai_handler.py b/pr_agent/algo/ai_handler.py index 57221518..27e9533a 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: """ @@ -69,15 +71,16 @@ class AiHandler: """ try: response = await acompletion( - model=model, - deployment_id=self.deployment_id, - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": user} - ], - temperature=temperature, - azure=self.azure - ) + model=model, + deployment_id=self.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 @@ -92,4 +95,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/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 From 886139c6b5634c2fd5a83f983822335bf2e943c8 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 7 Aug 2023 16:18:08 +0300 Subject: [PATCH 2/6] Support adding / removing reaction from comments in GitHub different servers --- pr_agent/git_providers/bitbucket_provider.py | 6 ++++++ pr_agent/git_providers/git_provider.py | 8 ++++++++ pr_agent/git_providers/github_provider.py | 21 ++++++++++++++++++-- pr_agent/git_providers/gitlab_provider.py | 6 ++++++ pr_agent/servers/github_action_runner.py | 6 ++++++ pr_agent/servers/github_app.py | 7 +++++++ pr_agent/servers/github_polling.py | 6 +++++- 7 files changed, 57 insertions(+), 3 deletions(-) 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..bc5cc6a7 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 @@ -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) 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..5c42527c 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 @@ -61,7 +62,12 @@ async def run_action(): pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url") if pr_url: body = comment_body.strip().lower() + comment_id = event_payload.get("comment", {}).get("id") + provider = get_git_provider()(pr_url=pr_url) + added_reaction = provider.add_eyes_reaction(comment_id) await PRAgent().handle_request(pr_url, body) + if added_reaction: + provider.remove_reaction(comment_id, added_reaction) if __name__ == '__main__': diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 263f5ba5..fe7fa354 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,13 @@ async def handle_request(body: Dict[str, Any]): return {} pull_request = body["issue"]["pull_request"] api_url = pull_request.get("url") + comment_id = body.get("comment", {}).get("id") + provider = get_git_provider()(pr_url=api_url) + added_reaction = provider.add_eyes_reaction(comment_id) await agent.handle_request(api_url, comment_body) + if added_reaction: + provider.remove_reaction(comment_id, added_reaction) + elif action == "opened" or 'reopened' in action: pull_request = body.get("pull_request") diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index 18f71dd7..007b3e9d 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -98,8 +98,12 @@ async def polling_loop(): if user_tag not in comment_body: continue rest_of_comment = comment_body.split(user_tag)[1].strip() - + comment_id = comment['id'] + git_provider.set_pr(pr_url) + added_reaction = git_provider.add_eyes_reaction(comment_id) success = await agent.handle_request(pr_url, rest_of_comment) + if added_reaction: + git_provider.remove_reaction(comment_id, added_reaction) if not success: git_provider.set_pr(pr_url) git_provider.publish_comment("### How to use PR-Agent\n" + From a75253097b0a43f9cd6b136850c45b1152fecf7c Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 7 Aug 2023 16:28:20 +0300 Subject: [PATCH 3/6] Don't remove eyes --- pr_agent/servers/github_action_runner.py | 4 +--- pr_agent/servers/github_app.py | 4 +--- pr_agent/servers/github_polling.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 5c42527c..52e3b138 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -64,10 +64,8 @@ async def run_action(): body = comment_body.strip().lower() comment_id = event_payload.get("comment", {}).get("id") provider = get_git_provider()(pr_url=pr_url) - added_reaction = provider.add_eyes_reaction(comment_id) + provider.add_eyes_reaction(comment_id) await PRAgent().handle_request(pr_url, body) - if added_reaction: - provider.remove_reaction(comment_id, added_reaction) if __name__ == '__main__': diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index fe7fa354..1446b7e5 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -83,10 +83,8 @@ async def handle_request(body: Dict[str, Any]): api_url = pull_request.get("url") comment_id = body.get("comment", {}).get("id") provider = get_git_provider()(pr_url=api_url) - added_reaction = provider.add_eyes_reaction(comment_id) + provider.add_eyes_reaction(comment_id) await agent.handle_request(api_url, comment_body) - if added_reaction: - provider.remove_reaction(comment_id, added_reaction) elif action == "opened" or 'reopened' in action: diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index 007b3e9d..773b34f3 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -100,10 +100,8 @@ async def polling_loop(): rest_of_comment = comment_body.split(user_tag)[1].strip() comment_id = comment['id'] git_provider.set_pr(pr_url) - added_reaction = git_provider.add_eyes_reaction(comment_id) + git_provider.add_eyes_reaction(comment_id) success = await agent.handle_request(pr_url, rest_of_comment) - if added_reaction: - git_provider.remove_reaction(comment_id, added_reaction) if not success: git_provider.set_pr(pr_url) git_provider.publish_comment("### How to use PR-Agent\n" + From b1148e5f7aa95e3640f24396badd3804f49e3b2b Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 7 Aug 2023 16:34:28 +0300 Subject: [PATCH 4/6] Don't commment on Github, only eyes reaction --- pr_agent/servers/github_action_runner.py | 1 + pr_agent/servers/github_app.py | 1 + pr_agent/servers/github_polling.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 52e3b138..9950791a 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -15,6 +15,7 @@ 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: diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 1446b7e5..53419b6e 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -107,6 +107,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 773b34f3..da5cd83e 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 From 164ed77d72d961efa4d777b9e1bccdc21150437d Mon Sep 17 00:00:00 2001 From: Zohar Meir <33152084+zmeir@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:09:50 +0300 Subject: [PATCH 5/6] Attempt to fix bug in create_inline_comment --- pr_agent/git_providers/github_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index bc5cc6a7..f83216ef 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -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}") @@ -410,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 "" From ebbe655c403c7affd75cbd629322d0983a391048 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 7 Aug 2023 18:09:39 +0300 Subject: [PATCH 6/6] Don't commment on Github, only eyes reaction --- pr_agent/agent/pr_agent.py | 6 +++++- pr_agent/servers/github_action_runner.py | 4 ++-- pr_agent/servers/github_app.py | 3 +-- pr_agent/servers/github_polling.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) 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/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 9950791a..fbf4f89c 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -17,6 +17,7 @@ async def run_action(): 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: print("GITHUB_EVENT_NAME not set") @@ -65,8 +66,7 @@ async def run_action(): body = comment_body.strip().lower() comment_id = event_payload.get("comment", {}).get("id") provider = get_git_provider()(pr_url=pr_url) - provider.add_eyes_reaction(comment_id) - await PRAgent().handle_request(pr_url, body) + 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 53419b6e..18943ae8 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -83,8 +83,7 @@ async def handle_request(body: Dict[str, Any]): api_url = pull_request.get("url") comment_id = body.get("comment", {}).get("id") provider = get_git_provider()(pr_url=api_url) - provider.add_eyes_reaction(comment_id) - await agent.handle_request(api_url, comment_body) + await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id)) elif action == "opened" or 'reopened' in action: diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index da5cd83e..fdd6642d 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -101,8 +101,8 @@ async def polling_loop(): rest_of_comment = comment_body.split(user_tag)[1].strip() comment_id = comment['id'] git_provider.set_pr(pr_url) - git_provider.add_eyes_reaction(comment_id) - success = await agent.handle_request(pr_url, rest_of_comment) + 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" +