diff --git a/README.md b/README.md index a33fa7cf3..5de410b12 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Making pull requests less painful with an AI agent
-CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of commands: +CodiumAI `PR-Agent` is an open-source tool for efficient pull request reviewing and handling. It automatically analyzes the pull request and can provide several types of commands: ‣ **Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels. \ @@ -33,17 +33,17 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull \ ‣ **Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes. \ -‣ **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues +‣ **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues. \ ‣ **Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR. \ ‣ **Generate Custom Labels ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes. -See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms. +See the [Installation Guide](./INSTALL.md) for instructions on installing and running the tool on different git platforms. -See the [Usage Guide](./Usage.md) for instructions how to run the different tools from _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened. +See the [Usage Guide](./Usage.md) for running the PR-Agent commands via different interfaces, including _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened. -See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools. +See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools (tools are run via the commands).

Example results:

@@ -140,7 +140,7 @@ Review the [usage guide](./Usage.md) section for detailed instructions how to us ## Try it now -You can try GPT-4 powered PR-Agent, on your public GitHub repository, instantly. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command. +Try the GPT-4 powered PR-Agent instantly on _your public GitHub repository_. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command. For example, add a comment to any pull request with the following text: ``` @CodiumAI-Agent /review @@ -151,6 +151,7 @@ and the agent will respond with a review of your PR To set up your own PR-Agent, see the [Installation](#installation) section below. +Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there is no need to mention `@CodiumAI-Agent ...`. Instead, directly start with the command, e.g., `/ask ...`. --- diff --git a/Usage.md b/Usage.md index a7c64fbc0..db9874d2f 100644 --- a/Usage.md +++ b/Usage.md @@ -239,6 +239,20 @@ inline_code_comments = true Each time you invoke a `/review` tool, it will use inline code comments. +#### BitBucket Self-Hosted App automatic tools +You can configure in your local `.pr_agent.toml` file which tools will **run automatically** when a new PR is opened. + +Specifically, set the following values: +```yaml +[bitbucket_app] +auto_review = true # set as config var in .pr_agent.toml +auto_describe = true # set as config var in .pr_agent.toml +auto_improve = true # set as config var in .pr_agent.toml +``` + +`bitbucket_app.auto_review`, `bitbucket_app.auto_describe` and `bitbucket_app.auto_improve` are used to enable/disable automatic tools. +If not set, the default option is that only the `review` tool will run automatically when a new PR is opened. + ### Changing a model See [here](pr_agent/algo/__init__.py) for the list of available models. diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index ee8ad48fc..23173f8e0 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -354,5 +354,5 @@ def publish_labels(self, pr_types: list): pass # bitbucket does not support labels - def get_labels(self): + def get_pr_labels(self): pass diff --git a/pr_agent/git_providers/bitbucket_server_provider.py b/pr_agent/git_providers/bitbucket_server_provider.py index 44347850b..902beb16a 100644 --- a/pr_agent/git_providers/bitbucket_server_provider.py +++ b/pr_agent/git_providers/bitbucket_server_provider.py @@ -344,7 +344,7 @@ def publish_labels(self, pr_types: list): pass # bitbucket does not support labels - def get_labels(self): + def get_pr_labels(self): pass def _get_pr_comments_url(self): diff --git a/pr_agent/git_providers/codecommit_provider.py b/pr_agent/git_providers/codecommit_provider.py index 64cfc70a0..286444c50 100644 --- a/pr_agent/git_providers/codecommit_provider.py +++ b/pr_agent/git_providers/codecommit_provider.py @@ -216,7 +216,7 @@ def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_labels(self, labels): return [""] # not implemented yet - def get_labels(self): + def get_pr_labels(self): return [""] # not implemented yet def remove_initial_comment(self): diff --git a/pr_agent/git_providers/gerrit_provider.py b/pr_agent/git_providers/gerrit_provider.py index d286b1bf1..dbdbe82f8 100644 --- a/pr_agent/git_providers/gerrit_provider.py +++ b/pr_agent/git_providers/gerrit_provider.py @@ -207,7 +207,7 @@ def get_issue_comments(self): Comment = namedtuple('Comment', ['body']) return Comments([Comment(c['message']) for c in reversed(comments)]) - def get_labels(self): + def get_pr_labels(self): raise NotImplementedError( 'Getting labels is not implemented for the gerrit provider') diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index deb5df3d8..4c4684c30 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -135,7 +135,10 @@ def publish_labels(self, labels): pass @abstractmethod - def get_labels(self): + def get_pr_labels(self): + pass + + def get_repo_labels(self): pass @abstractmethod diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 3ae977429..f365db848 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -461,13 +461,17 @@ def publish_labels(self, pr_types): except Exception as e: get_logger().exception(f"Failed to publish labels, error: {e}") - def get_labels(self): + def get_pr_labels(self): try: return [label.name for label in self.pr.labels] except Exception as e: get_logger().exception(f"Failed to get labels, error: {e}") return [] + def get_repo_labels(self): + labels = self.repo_obj.get_labels() + return [label for label in labels] + def get_commit_messages(self): """ Retrieves the commit messages of a pull request. diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 7b11ef54e..618cebc02 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -211,7 +211,11 @@ def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: pos_obj['new_line'] = target_line_no - 1 pos_obj['old_line'] = source_line_no - 1 get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") - self.mr.discussions.create({'body': body, 'position': pos_obj}) + try: + self.mr.discussions.create({'body': body, 'position': pos_obj}) + except Exception as e: + get_logger().debug( + f"Failed to create comment in {self.id_mr} with position {pos_obj} (probably not a '+' line)") def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: changes = self.mr.changes() # Retrieve the changes for the merge request once @@ -404,7 +408,7 @@ def publish_labels(self, pr_types): def publish_inline_comments(self, comments: list[dict]): pass - def get_labels(self): + def get_pr_labels(self): return self.mr.labels def get_commit_messages(self): diff --git a/pr_agent/git_providers/local_git_provider.py b/pr_agent/git_providers/local_git_provider.py index 0ef11413b..b3fad772a 100644 --- a/pr_agent/git_providers/local_git_provider.py +++ b/pr_agent/git_providers/local_git_provider.py @@ -178,5 +178,5 @@ def get_pr_title(self): def get_issue_comments(self): raise NotImplementedError('Getting issue comments is not implemented for the local git provider') - def get_labels(self): + def get_pr_labels(self): raise NotImplementedError('Getting labels is not implemented for the local git provider') diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index 739ba162a..9fba4030d 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -16,8 +16,13 @@ from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings, global_settings +from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.secret_providers import get_secret_provider +from pr_agent.servers.github_action_runner import get_setting_or_env, is_true +from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions +from pr_agent.tools.pr_description import PRDescription +from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAiHandler litellm_ai_handler = LiteLLMAiHandler() @@ -91,8 +96,20 @@ async def inner(): pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] log_context["api_url"] = pr_url log_context["event"] = "pull_request" - with get_logger().contextualize(**log_context): - await agent.handle_request(pr_url, "review") + if pr_url: + with get_logger().contextualize(**log_context): + apply_repo_settings(pr_url) + auto_review = get_setting_or_env("BITBUCKET_APP.AUTO_REVIEW", None) + if auto_review is None or is_true(auto_review): # by default, auto review is enabled + await PRReviewer(pr_url).run() + auto_improve = get_setting_or_env("BITBUCKET_APP.AUTO_IMPROVE", None) + if is_true(auto_improve): # by default, auto improve is disabled + await PRCodeSuggestions(pr_url).run() + auto_describe = get_setting_or_env("BITBUCKET_APP.AUTO_DESCRIBE", None) + if is_true(auto_describe): # by default, auto describe is disabled + await PRDescription(pr_url).run() + # with get_logger().contextualize(**log_context): + # await agent.handle_request(pr_url, "review") elif event == "pullrequest:comment_created": pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] log_context["api_url"] = pr_url @@ -139,7 +156,6 @@ async def handle_uninstalled_webhooks(request: Request, response: Response): def start(): get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) get_settings().set("CONFIG.GIT_PROVIDER", "bitbucket") - get_settings().set("PR_DESCRIPTION.PUBLISH_DESCRIPTION_AS_COMMENT", True) middleware = [Middleware(RawContextMiddleware)] app = FastAPI(middleware=middleware) app.include_router(router) diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 00b32b947..bdcd78f50 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -127,11 +127,15 @@ async def handle_request(body: Dict[str, Any], event: str): await _perform_commands("pr_commands", agent, body, api_url, log_context) # handle pull_request event with synchronize action - "push trigger" for new commits - elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger: + elif event == 'pull_request' and action == 'synchronize': pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user) if not (pull_request and api_url): return {} + apply_repo_settings(api_url) + if not get_settings().github_app.handle_push_trigger: + return {} + # TODO: do we still want to get the list of commits to filter bot/merge commits? before_sha = body.get("before") after_sha = body.get("after") diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 259383d7e..30fc97f88 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -143,6 +143,12 @@ magic_word = "AutoReview" # Polling interval polling_interval_seconds = 30 +[bitbucket_app] +#auto_review = true # set as config var in .pr_agent.toml +#auto_describe = true # set as config var in .pr_agent.toml +#auto_improve = true # set as config var in .pr_agent.toml + + [local] # LocalGitProvider settings - uncomment to use paths other than default # description_path= "path/to/description.md" @@ -170,3 +176,4 @@ max_issues_to_scan = 500 # fill and place in .secrets.toml #api_key = ... # environment = "gcp-starter" + diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 73807cf52..8d9623cd7 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -102,11 +102,12 @@ async def run(self): if get_settings().config.publish_output: get_logger().info(f"Pushing answer {self.pr_id}") if get_settings().pr_description.publish_description_as_comment: + get_logger().info(f"Publishing answer as comment") self.git_provider.publish_comment(full_markdown_description) else: self.git_provider.publish_description(pr_title, pr_body) if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"): - current_labels = self.git_provider.get_labels() + current_labels = self.git_provider.get_pr_labels() user_labels = get_user_labels(current_labels) self.git_provider.publish_labels(pr_labels + user_labels) @@ -158,7 +159,7 @@ async def _get_prediction(self, model: str) -> str: variables["diff"] = self.patches_diff # update diff environment = Environment(undefined=StrictUndefined) - set_custom_labels(variables) + set_custom_labels(variables, self.git_provider) system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables) @@ -290,7 +291,7 @@ def _prepare_pr_answer(self) -> Tuple[str, str]: value = ', '.join(v for v in value) pr_body += f"{value}\n" if idx < len(self.data) - 1: - pr_body += "\n___\n" + pr_body += "\n\n___\n\n" if get_settings().config.verbosity_level >= 2: get_logger().info(f"title:\n{title}\n{pr_body}") @@ -315,7 +316,6 @@ def process_pr_files_prediction(self, pr_body, value): if not self.git_provider.is_supported("gfm_markdown"): get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported") return pr_body - try: pr_body += "" header = f"Relevant files" diff --git a/pr_agent/tools/pr_generate_labels.py b/pr_agent/tools/pr_generate_labels.py index 6ea322a4e..fc90ed44f 100644 --- a/pr_agent/tools/pr_generate_labels.py +++ b/pr_agent/tools/pr_generate_labels.py @@ -82,7 +82,7 @@ async def run(self): if get_settings().config.publish_output: get_logger().info(f"Pushing labels {self.pr_id}") - current_labels = self.git_provider.get_labels() + current_labels = self.git_provider.get_pr_labels() user_labels = get_user_labels(current_labels) pr_labels = pr_labels + user_labels @@ -132,7 +132,7 @@ async def _get_prediction(self, model: str) -> str: variables["diff"] = self.patches_diff # update diff environment = Environment(undefined=StrictUndefined) - set_custom_labels(variables) + set_custom_labels(variables, self.git_provider) system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index d86dc052f..ca345cbaa 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -394,7 +394,7 @@ def set_review_labels(self, data): if security_concerns_bool: review_labels.append('Possible security concern') - current_labels = self.git_provider.get_labels() + current_labels = self.git_provider.get_pr_labels() current_labels_filtered = [label for label in current_labels if not label.lower().startswith('review effort [1-5]:') and not label.lower().startswith( 'possible security concern')] diff --git a/tests/unittest/test_convert_to_markdown.py b/tests/unittest/test_convert_to_markdown.py index b03c4fdec..800d3ada8 100644 --- a/tests/unittest/test_convert_to_markdown.py +++ b/tests/unittest/test_convert_to_markdown.py @@ -71,7 +71,7 @@ def test_simple_dictionary_input(self): - 📌 **Type of PR:** Test type\n\ - 🧪 **Relevant tests added:** no\n\ - ✨ **Focused PR:** Yes\n\ -- **General PR suggestions:** general suggestion...\n\n\n-
🤖 Code feedback:\n\n - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n
\ +- **General PR suggestions:** general suggestion...\n\n\n-
🤖 Code feedback: - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n
\ """ assert convert_to_markdown(input_data).strip() == expected_output.strip()