.atlassian.net"
+ ```
+
+#### Jira Cloud 💎
+There are two ways to authenticate with Jira Cloud:
+
+**1) Jira App Authentication**
+
+The recommended way to authenticate with Jira Cloud is to install the Qodo Merge app in your Jira Cloud instance. This will allow Qodo Merge to access Jira data on your behalf.
+
+Installation steps:
+
+1. Click [here](https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=8krKmA4gMD8mM8z24aRCgPCSepZNP1xf&scope=read%3Ajira-work%20offline_access&redirect_uri=https%3A%2F%2Fregister.jira.pr-agent.codium.ai&state=qodomerge&response_type=code&prompt=consent) to install the Qodo Merge app in your Jira Cloud instance, click the `accept` button.
+{width=384}
+
+2. After installing the app, you will be redirected to the Qodo Merge registration page. and you will see a success message.
+{width=384}
+
+3. Now you can use the Jira integration in Qodo Merge PR Agent.
+
+**2) Email/Token Authentication**
+
+You can create an API token from your Atlassian account:
+
+1. Log in to https://id.atlassian.com/manage-profile/security/api-tokens.
+
+2. Click Create API token.
+
+3. From the dialog that appears, enter a name for your new token and click Create.
+
+4. Click Copy to clipboard.
+
+{width=384}
+
+5. In your [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/) add the following lines:
+
+```toml
+[jira]
+jira_api_token = "YOUR_API_TOKEN"
+jira_api_email = "YOUR_EMAIL"
+```
+
+
+#### Jira Server/Data Center 💎
+
+Currently, we only support the Personal Access Token (PAT) Authentication method.
+
+1. Create a [Personal Access Token (PAT)](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) in your Jira account
+2. In your Configuration file/Environment variables/Secrets file, add the following lines:
+
+```toml
+[jira]
+jira_base_url = "YOUR_JIRA_BASE_URL" # e.g. https://jira.example.com
+jira_api_token = "YOUR_API_TOKEN"
+```
\ No newline at end of file
diff --git a/docs/docs/core-abilities/index.md b/docs/docs/core-abilities/index.md
index 2d025c9d..b8895517 100644
--- a/docs/docs/core-abilities/index.md
+++ b/docs/docs/core-abilities/index.md
@@ -1,6 +1,7 @@
# Core Abilities
Qodo Merge utilizes a variety of core abilities to provide a comprehensive and efficient code review experience. These abilities include:
+- [Fetching ticket context](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/)
- [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/)
- [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/)
- [Self-reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/)
diff --git a/docs/docs/core-abilities/self_reflection.md b/docs/docs/core-abilities/self_reflection.md
index 894cee69..0a409128 100644
--- a/docs/docs/core-abilities/self_reflection.md
+++ b/docs/docs/core-abilities/self_reflection.md
@@ -46,6 +46,5 @@ This results in a more refined and valuable set of suggestions for the user, sav
## Appendix - Relevant Configuration Options
```
[pr_code_suggestions]
-self_reflect_on_suggestions = true # Enable self-reflection on code suggestions
suggestions_score_threshold = 0 # Filter out suggestions with a score below this threshold (0-10)
```
diff --git a/docs/docs/installation/bitbucket.md b/docs/docs/installation/bitbucket.md
index 8648803f..346c86f8 100644
--- a/docs/docs/installation/bitbucket.md
+++ b/docs/docs/installation/bitbucket.md
@@ -3,7 +3,7 @@
You can use the Bitbucket Pipeline system to run Qodo Merge on every pull request open or update.
-1. Add the following file in your repository bitbucket_pipelines.yml
+1. Add the following file in your repository bitbucket-pipelines.yml
```yaml
pipelines:
diff --git a/docs/docs/tools/improve.md b/docs/docs/tools/improve.md
index bbc98e4a..0d5ff534 100644
--- a/docs/docs/tools/improve.md
+++ b/docs/docs/tools/improve.md
@@ -276,12 +276,12 @@ Using a combination of both can help the AI model to provide relevant and tailor
Minimum score threshold for suggestions to be presented as commitable PR comments in addition to the table. Default is -1 (disabled). |
- persistent_comment |
- If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false. |
+ focus_only_on_problems |
+ If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is false. |
- self_reflect_on_suggestions |
- If set to true, the improve tool will calculate an importance score for each suggestion [1-10], and sort the suggestion labels group based on this score. Default is true. |
+ persistent_comment |
+ If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false. |
suggestions_score_threshold |
diff --git a/docs/docs/tools/review.md b/docs/docs/tools/review.md
index 5d70c9e4..98f52928 100644
--- a/docs/docs/tools/review.md
+++ b/docs/docs/tools/review.md
@@ -140,7 +140,7 @@ num_code_suggestions = ...
require_ticket_analysis_review |
- If set to true, and the PR contains a GitHub ticket number, the tool will add a section that checks if the PR in fact fulfilled the ticket requirements. Default is true. |
+ If set to true, and the PR contains a GitHub or Jira ticket link, the tool will add a section that checks if the PR in fact fulfilled the ticket requirements. Default is true. |
diff --git a/docs/docs/usage-guide/additional_configurations.md b/docs/docs/usage-guide/additional_configurations.md
index 89573d3f..72aa99b8 100644
--- a/docs/docs/usage-guide/additional_configurations.md
+++ b/docs/docs/usage-guide/additional_configurations.md
@@ -160,3 +160,13 @@ ignore_pr_target_branches = ["qa"]
Where the `ignore_pr_source_branches` and `ignore_pr_target_branches` are lists of regex patterns to match the source and target branches you want to ignore.
They are not mutually exclusive, you can use them together or separately.
+
+
+To allow only specific folders (often needed in large monorepos), set:
+
+```
+[config]
+allow_only_specific_folders=['folder1','folder2']
+```
+
+For the configuration above, automatic feedback will only be triggered when the PR changes include files from 'folder1' or 'folder2'
diff --git a/docs/docs/usage-guide/automations_and_usage.md b/docs/docs/usage-guide/automations_and_usage.md
index 6fca67ec..007f5bf2 100644
--- a/docs/docs/usage-guide/automations_and_usage.md
+++ b/docs/docs/usage-guide/automations_and_usage.md
@@ -72,13 +72,14 @@ The configuration parameter `pr_commands` defines the list of tools that will be
```
[github_app]
pr_commands = [
- "/describe --pr_description.final_update_message=false",
- "/review --pr_reviewer.num_code_suggestions=0",
- "/improve",
+ "/describe",
+ "/review",
+ "/improve --pr_code_suggestions.suggestions_score_threshold=5",
]
```
-This means that when a new PR is opened/reopened or marked as ready for review, Qodo Merge will run the `describe`, `review` and `improve` tools.
-For the `review` tool, for example, the `num_code_suggestions` parameter will be set to 0.
+
+This means that when a new PR is opened/reopened or marked as ready for review, Qodo Merge will run the `describe`, `review` and `improve` tools.
+For the `improve` tool, for example, the `suggestions_score_threshold` parameter will be set to 5 (suggestions below a score of 5 won't be presented)
You can override the default tool parameters by using one the three options for a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/): **wiki**, **local**, or **global**.
For example, if your local `.pr_agent.toml` file contains:
@@ -105,7 +106,7 @@ The configuration parameter `push_commands` defines the list of tools that will
handle_push_trigger = true
push_commands = [
"/describe",
- "/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
+ "/review",
]
```
This means that when new code is pushed to the PR, the Qodo Merge will run the `describe` and `review` tools, with the specified parameters.
@@ -148,7 +149,7 @@ After setting up a GitLab webhook, to control which commands will run automatica
[gitlab]
pr_commands = [
"/describe",
- "/review --pr_reviewer.num_code_suggestions=0",
+ "/review",
"/improve",
]
```
@@ -161,7 +162,7 @@ The configuration parameter `push_commands` defines the list of tools that will
handle_push_trigger = true
push_commands = [
"/describe",
- "/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
+ "/review",
]
```
@@ -182,7 +183,7 @@ Each time you invoke a `/review` tool, it will use the extra instructions you se
Note that among other limitations, BitBucket provides relatively low rate-limits for applications (up to 1000 requests per hour), and does not provide an API to track the actual rate-limit usage.
-If you experience lack of responses from Qodo Merge, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
+If you experience a lack of responses from Qodo Merge, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
This will prevent Qodo Merge from acquiring the full file content, and will only use the diff content. This will reduce the number of requests made to BitBucket, at the cost of small decrease in accuracy, as dynamic context will not be applicable.
@@ -194,13 +195,23 @@ Specifically, set the following values:
```
[bitbucket_app]
pr_commands = [
- "/review --pr_reviewer.num_code_suggestions=0",
+ "/review",
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
]
```
Note that we set specifically for bitbucket, we recommend using: `--pr_code_suggestions.suggestions_score_threshold=7` and that is the default value we set for bitbucket.
Since this platform only supports inline code suggestions, we want to limit the number of suggestions, and only present a limited number.
+To enable BitBucket app to respond to each **push** to the PR, set (for example):
+```
+[bitbucket_app]
+handle_push_trigger = true
+push_commands = [
+ "/describe",
+ "/review",
+]
+```
+
## Azure DevOps provider
To use Azure DevOps provider use the following settings in configuration.toml:
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 53ae3c24..d98d5b41 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -43,6 +43,7 @@ nav:
- 💎 Similar Code: 'tools/similar_code.md'
- Core Abilities:
- 'core-abilities/index.md'
+ - Fetching ticket context: 'core-abilities/fetching_ticket_context.md'
- Local and global metadata: 'core-abilities/metadata.md'
- Dynamic context: 'core-abilities/dynamic_context.md'
- Self-reflection: 'core-abilities/self_reflection.md'
diff --git a/pr_agent/algo/__init__.py b/pr_agent/algo/__init__.py
index 83d001a1..2a9b1f34 100644
--- a/pr_agent/algo/__init__.py
+++ b/pr_agent/algo/__init__.py
@@ -31,6 +31,7 @@ MAX_TOKENS = {
'vertex_ai/codechat-bison': 6144,
'vertex_ai/codechat-bison-32k': 32000,
'vertex_ai/claude-3-haiku@20240307': 100000,
+ 'vertex_ai/claude-3-5-haiku@20241022': 100000,
'vertex_ai/claude-3-sonnet@20240229': 100000,
'vertex_ai/claude-3-opus@20240229': 100000,
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
@@ -48,11 +49,13 @@ MAX_TOKENS = {
'anthropic/claude-3-opus-20240229': 100000,
'anthropic/claude-3-5-sonnet-20240620': 100000,
'anthropic/claude-3-5-sonnet-20241022': 100000,
+ 'anthropic/claude-3-5-haiku-20241022': 100000,
'bedrock/anthropic.claude-instant-v1': 100000,
'bedrock/anthropic.claude-v2': 100000,
'bedrock/anthropic.claude-v2:1': 100000,
'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000,
'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000,
+ 'bedrock/anthropic.claude-3-5-haiku-20241022-v1:0': 100000,
'bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0': 100000,
'bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0': 100000,
'claude-3-5-sonnet': 100000,
diff --git a/pr_agent/algo/ai_handlers/openai_ai_handler.py b/pr_agent/algo/ai_handlers/openai_ai_handler.py
index b235af96..f74444a1 100644
--- a/pr_agent/algo/ai_handlers/openai_ai_handler.py
+++ b/pr_agent/algo/ai_handlers/openai_ai_handler.py
@@ -1,5 +1,7 @@
+from os import environ
+from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
import openai
-from openai.error import APIError, RateLimitError, Timeout, TryAgain
+from openai import APIError, AsyncOpenAI, RateLimitError, Timeout
from retry import retry
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
@@ -14,7 +16,7 @@ class OpenAIHandler(BaseAiHandler):
# Initialize OpenAIHandler specific attributes here
try:
super().__init__()
- openai.api_key = get_settings().openai.key
+ environ["OPENAI_API_KEY"] = get_settings().openai.key
if get_settings().get("OPENAI.ORG", None):
openai.organization = get_settings().openai.org
if get_settings().get("OPENAI.API_TYPE", None):
@@ -24,7 +26,7 @@ class OpenAIHandler(BaseAiHandler):
if get_settings().get("OPENAI.API_VERSION", None):
openai.api_version = get_settings().openai.api_version
if get_settings().get("OPENAI.API_BASE", None):
- openai.api_base = get_settings().openai.api_base
+ environ["OPENAI_BASE_URL"] = get_settings().openai.api_base
except AttributeError as e:
raise ValueError("OpenAI key is required") from e
@@ -36,28 +38,26 @@ class OpenAIHandler(BaseAiHandler):
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
- @retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
+ @retry(exceptions=(APIError, Timeout, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try:
- deployment_id = self.deployment_id
get_logger().info("System: ", system)
get_logger().info("User: ", user)
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
-
- chat_completion = await openai.ChatCompletion.acreate(
+ client = AsyncOpenAI()
+ chat_completion = await client.chat.completions.create(
model=model,
- deployment_id=deployment_id,
messages=messages,
temperature=temperature,
)
- resp = chat_completion["choices"][0]['message']['content']
- finish_reason = chat_completion["choices"][0]["finish_reason"]
- usage = chat_completion.get("usage")
+ resp = chat_completion.choices[0].message.content
+ finish_reason = chat_completion.choices[0].finish_reason
+ usage = chat_completion.usage
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage)
return resp, finish_reason
- except (APIError, Timeout, TryAgain) as e:
+ except (APIError, Timeout) as e:
get_logger().error("Error during OpenAI inference: ", e)
raise
except (RateLimitError) as e:
@@ -65,4 +65,4 @@ class OpenAIHandler(BaseAiHandler):
raise
except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e)
- raise TryAgain from e
+ raise
diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py
index a90636d3..3dac4e7b 100644
--- a/pr_agent/algo/utils.py
+++ b/pr_agent/algo/utils.py
@@ -1028,7 +1028,7 @@ def process_description(description_full: str) -> Tuple[str, List]:
if not description_full:
return "", []
- description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH)
+ description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH.value)
base_description_str = description_split[0]
changes_walkthrough_str = ""
files = []
diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py
index df87c564..d1f9b9ca 100644
--- a/pr_agent/git_providers/azuredevops_provider.py
+++ b/pr_agent/git_providers/azuredevops_provider.py
@@ -404,7 +404,7 @@ class AzureDevopsProvider(GitProvider):
pr_body = pr_body[:ind]
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
- changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH
+ changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH.value
ind = pr_body.find(changes_walkthrough_text)
if ind != -1:
pr_body = pr_body[:ind]
diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml
index 6d1f0cee..97d8d363 100644
--- a/pr_agent/settings/configuration.toml
+++ b/pr_agent/settings/configuration.toml
@@ -7,6 +7,7 @@ fallback_models=["gpt-4o-2024-05-13"]
git_provider="github"
publish_output=true
publish_output_progress=true
+publish_output_no_suggestions=true
verbosity_level=0 # 0,1,2
use_extra_bad_extensions=false
# Configurations
@@ -106,10 +107,11 @@ enable_help_text=false
[pr_code_suggestions] # /improve #
-max_context_tokens=14000
+max_context_tokens=16000
#
commitable_code_suggestions = false
dual_publishing_score_threshold=-1 # -1 to disable, [0-10] to set the threshold (>=) for publishing a code suggestion both in a table and as commitable
+focus_only_on_problems=false
#
extra_instructions = ""
rank_suggestions = false
@@ -121,7 +123,6 @@ max_history_len=4
# enable to apply suggestion 💎
apply_suggestions_checkbox=true
# suggestions scoring
-self_reflect_on_suggestions=true
suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions
# params for '/improve --extended' mode
auto_extended_mode=true
diff --git a/pr_agent/settings/pr_code_suggestions_prompts.toml b/pr_agent/settings/pr_code_suggestions_prompts.toml
index be47b3ce..012ae0fc 100644
--- a/pr_agent/settings/pr_code_suggestions_prompts.toml
+++ b/pr_agent/settings/pr_code_suggestions_prompts.toml
@@ -1,7 +1,10 @@
[pr_code_suggestions_prompt]
system="""You are PR-Reviewer, an AI specializing in Pull Request (PR) code analysis and suggestions.
-Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix possible bugs and problems, and enhance code quality, readability, and performance.
-
+{%- if not focus_only_on_problems %}
+Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix possible bugs and problems, and enhance code quality and performance.
+{%- else %}
+Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix critical bugs and problems.
+{%- endif %}
The PR code diff will be in the following structured format:
======
@@ -14,10 +17,10 @@ The PR code diff will be in the following structured format:
@@ ... @@ def func1():
__new hunk__
-11 unchanged code line0 in the PR
-12 unchanged code line1 in the PR
-13 +new code line2 added in the PR
-14 unchanged code line3 in the PR
+ unchanged code line0 in the PR
+ unchanged code line1 in the PR
++new code line2 added in the PR
+ unchanged code line3 in the PR
__old hunk__
unchanged code line0
unchanged code line1
@@ -35,7 +38,6 @@ __new hunk__
======
- In the format above, the diff is organized into separate '__new hunk__' and '__old hunk__' sections for each code chunk. '__new hunk__' contains the updated code, while '__old hunk__' shows the removed code. If no code was removed in a specific chunk, the __old hunk__ section will be omitted.
-- Line numbers were added for the '__new hunk__' sections to help referencing specific lines in the code suggestions. These numbers are for reference only and are not part of the actual code.
- Code lines are prefixed with symbols: '+' for new code added in the PR, '-' for code removed, and ' ' for unchanged code.
{%- if is_ai_metadata %}
- When available, an AI-generated summary will precede each file's diff, with a high-level overview of the changes. Note that this summary may not be fully accurate or complete.
@@ -43,9 +45,17 @@ __new hunk__
Specific guidelines for generating code suggestions:
+{%- if not focus_only_on_problems %}
- Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions.
-- Focus solely on enhancing new code introduced in the PR, identified by '+' prefixes in '__new hunk__' sections (after the line numbers).
+{%- else %}
+- Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions. Return less suggestions if no pertinent ones are applicable.
+{%- endif %}
+- Focus solely on enhancing new code introduced in the PR, identified by '+' prefixes in '__new hunk__' sections.
+{%- if not focus_only_on_problems %}
- Prioritize suggestions that address potential issues, critical problems, and bugs in the PR code. Avoid repeating changes already implemented in the PR. If no pertinent suggestions are applicable, return an empty list.
+{%- else %}
+- Only give suggestions that address critical problems and bugs in the PR code. If no relevant suggestions are applicable, return an empty list.
+{%- endif %}
- Don't suggest to add docstring, type hints, or comments, to remove unused imports, or to use more specific exception types.
- When referencing variables or names from the code, enclose them in backticks (`). Example: "ensure that `variable_name` is..."
- Be mindful you are viewing a partial PR code diff, not the full codebase. Avoid suggestions that might conflict with unseen code or alerting variables not declared in the visible scope, as the context is incomplete.
@@ -67,12 +77,14 @@ class CodeSuggestion(BaseModel):
relevant_file: str = Field(description="Full path of the relevant file")
language: str = Field(description="Programming language used by the relevant file")
suggestion_content: str = Field(description="An actionable suggestion to enhance, improve or fix the new code introduced in the PR. Don't present here actual code snippets, just the suggestion. Be short and concise")
- existing_code: str = Field(description="A short code snippet from a '__new hunk__' section that the suggestion aims to enhance or fix. Include only complete code lines, without line numbers. Use ellipsis (...) for brevity if needed. This snippet should represent the specific PR code targeted for improvement.")
+ existing_code: str = Field(description="A short code snippet from a '__new hunk__' section that the suggestion aims to enhance or fix. Include only complete code lines. Use ellipsis (...) for brevity if needed. This snippet should represent the specific PR code targeted for improvement.")
improved_code: str = Field(description="A refined code snippet that replaces the 'existing_code' snippet after implementing the suggestion.")
one_sentence_summary: str = Field(description="A concise, single-sentence overview of the suggested improvement. Focus on the 'what'. Be general, and avoid method or variable names.")
- relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the beginning of the 'existing code' snippet above")
- relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the end of the 'existing code' snippet above")
- label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability'. Other relevant labels are also acceptable.")
+{%- if not focus_only_on_problems %}
+ label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability', 'typo'. Other relevant labels are also acceptable.")
+{%- else %}
+ label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'critical bug', 'general'. The 'general' section should be used for suggestions that address a major issue, but are necessarily on a critical level.")
+{%- endif %}
class PRCodeSuggestions(BaseModel):
@@ -95,8 +107,6 @@ code_suggestions:
...
one_sentence_summary: |
...
- relevant_lines_start: 12
- relevant_lines_end: 13
label: |
...
```
@@ -112,7 +122,7 @@ Title: '{{title}}'
The PR Diff:
======
-{{ diff|trim }}
+{{ diff_no_line_numbers|trim }}
======
diff --git a/pr_agent/settings/pr_code_suggestions_reflect_prompts.toml b/pr_agent/settings/pr_code_suggestions_reflect_prompts.toml
index 512ec592..34b1eec4 100644
--- a/pr_agent/settings/pr_code_suggestions_reflect_prompts.toml
+++ b/pr_agent/settings/pr_code_suggestions_reflect_prompts.toml
@@ -15,8 +15,8 @@ Be particularly vigilant for suggestions that:
- Contradict or ignore parts of the PR's modifications
In such cases, assign the suggestion a score of 0.
-For valid suggestions, your role is to provide an impartial and precise score assessment that accurately reflects each suggestion's potential impact on the PR's correctness, quality and functionality.
-
+Evaluate each valid suggestion by scoring its potential impact on the PR's correctness, quality and functionality.
+In addition, you should also detect the line numbers in the '__new hunk__' section that correspond to the 'existing_code' snippet.
Key guidelines for evaluation:
- Thoroughly examine both the suggestion content and the corresponding PR code diff. Be vigilant for potential errors in each suggestion, ensuring they are logically sound, accurate, and directly derived from the PR code diff.
@@ -82,6 +82,8 @@ The output must be a YAML object equivalent to type $PRCodeSuggestionsFeedback,
class CodeSuggestionFeedback(BaseModel):
suggestion_summary: str = Field(description="Repeated from the input")
relevant_file: str = Field(description="Repeated from the input")
+ relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the beginning of the relevant 'existing code' snippet")
+ relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the end of the relevant 'existing code' snippet")
suggestion_score: int = Field(description="Evaluate the suggestion and assign a score from 0 to 10. Give 0 if the suggestion is wrong. For valid suggestions, score from 1 (lowest impact/importance) to 10 (highest impact/importance).")
why: str = Field(description="Briefly explain the score given in 1-2 sentences, focusing on the suggestion's impact, relevance, and accuracy.")
@@ -96,6 +98,8 @@ code_suggestions:
- suggestion_summary: |
Use a more descriptive variable name here
relevant_file: "src/file1.py"
+ relevant_lines_start: 13
+ relevant_lines_end: 14
suggestion_score: 6
why: |
The variable name 't' is not descriptive enough
diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py
index ae3efbb2..e2bc279e 100644
--- a/pr_agent/tools/pr_code_suggestions.py
+++ b/pr_agent/tools/pr_code_suggestions.py
@@ -3,6 +3,7 @@ import copy
import difflib
import re
import textwrap
+import traceback
from functools import partial
from typing import Dict, List
@@ -48,7 +49,7 @@ class PRCodeSuggestions:
self.is_extended = self._get_is_extended(args or [])
except:
self.is_extended = False
- num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk
+ num_code_suggestions = int(get_settings().pr_code_suggestions.num_code_suggestions_per_chunk)
self.ai_handler = ai_handler()
@@ -73,11 +74,13 @@ class PRCodeSuggestions:
"description": self.pr_description,
"language": self.main_language,
"diff": "", # empty diff for initial calculation
+ "diff_no_line_numbers": "", # empty diff for initial calculation
"num_code_suggestions": num_code_suggestions,
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(),
"relevant_best_practices": "",
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
+ "focus_only_on_problems": get_settings().get("pr_code_suggestions.focus_only_on_problems", False),
}
self.pr_code_suggestions_prompt_system = get_settings().pr_code_suggestions_prompt.system
@@ -114,15 +117,17 @@ class PRCodeSuggestions:
if not data:
data = {"code_suggestions": []}
- if (data is None or 'code_suggestions' not in data or not data['code_suggestions']
- and get_settings().config.publish_output):
- get_logger().warning('No code suggestions found for the PR.')
+ if (data is None or 'code_suggestions' not in data or not data['code_suggestions']):
pr_body = "## PR Code Suggestions ✨\n\nNo code suggestions found for the PR."
- get_logger().debug(f"PR output", artifact=pr_body)
- if self.progress_response:
- self.git_provider.edit_comment(self.progress_response, body=pr_body)
+ get_logger().warning('No code suggestions found for the PR.')
+ if get_settings().config.publish_output and get_settings().config.publish_output_no_suggestions:
+ get_logger().debug(f"PR output", artifact=pr_body)
+ if self.progress_response:
+ self.git_provider.edit_comment(self.progress_response, body=pr_body)
+ else:
+ self.git_provider.publish_comment(pr_body)
else:
- self.git_provider.publish_comment(pr_body)
+ get_settings().data = {"artifact": ""}
return
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
@@ -199,8 +204,11 @@ class PRCodeSuggestions:
self.git_provider.remove_comment(self.progress_response)
else:
get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.')
+ get_settings().data = {"artifact": data}
+ return
except Exception as e:
- get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
+ get_logger().error(f"Failed to generate code suggestions for PR, error: {e}",
+ artifact={"traceback": traceback.format_exc()})
if get_settings().config.publish_output:
if self.progress_response:
self.progress_response.delete()
@@ -332,7 +340,7 @@ class PRCodeSuggestions:
if self.patches_diff:
get_logger().debug(f"PR diff", artifact=self.patches_diff)
- self.prediction = await self._get_prediction(model, self.patches_diff)
+ self.prediction = await self._get_prediction(model, self.patches_diff, self.patches_diff_no_line_number)
else:
get_logger().warning(f"Empty PR diff")
self.prediction = None
@@ -340,42 +348,76 @@ class PRCodeSuggestions:
data = self.prediction
return data
- async def _get_prediction(self, model: str, patches_diff: str) -> dict:
+ async def _get_prediction(self, model: str, patches_diff: str, patches_diff_no_line_number: str) -> dict:
variables = copy.deepcopy(self.vars)
variables["diff"] = patches_diff # update diff
+ variables["diff_no_line_numbers"] = patches_diff_no_line_number # update diff
environment = Environment(undefined=StrictUndefined)
system_prompt = environment.from_string(self.pr_code_suggestions_prompt_system).render(variables)
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
response, finish_reason = await self.ai_handler.chat_completion(
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
+ if not get_settings().config.publish_output:
+ get_settings().system_prompt = system_prompt
+ get_settings().user_prompt = user_prompt
# load suggestions from the AI response
data = self._prepare_pr_code_suggestions(response)
- # self-reflect on suggestions
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- model_turbo = get_settings().config.model_turbo # use turbo model for self-reflection, since it is an easier task
- response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"],
- patches_diff, model=model_turbo)
- if response_reflect:
- response_reflect_yaml = load_yaml(response_reflect)
- code_suggestions_feedback = response_reflect_yaml["code_suggestions"]
- if len(code_suggestions_feedback) == len(data["code_suggestions"]):
- for i, suggestion in enumerate(data["code_suggestions"]):
- try:
- suggestion["score"] = code_suggestions_feedback[i]["suggestion_score"]
- suggestion["score_why"] = code_suggestions_feedback[i]["why"]
- except Exception as e: #
- get_logger().error(f"Error processing suggestion score {i}",
- artifact={"suggestion": suggestion,
- "code_suggestions_feedback": code_suggestions_feedback[i]})
- suggestion["score"] = 7
- suggestion["score_why"] = ""
- else:
- # get_logger().error(f"Could not self-reflect on suggestions. using default score 7")
+ # self-reflect on suggestions (mandatory, since line numbers are generated now here)
+ model_reflection = get_settings().config.model
+ response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"],
+ patches_diff, model=model_reflection)
+ if response_reflect:
+ response_reflect_yaml = load_yaml(response_reflect)
+ code_suggestions_feedback = response_reflect_yaml["code_suggestions"]
+ if len(code_suggestions_feedback) == len(data["code_suggestions"]):
for i, suggestion in enumerate(data["code_suggestions"]):
- suggestion["score"] = 7
- suggestion["score_why"] = ""
+ try:
+ suggestion["score"] = code_suggestions_feedback[i]["suggestion_score"]
+ suggestion["score_why"] = code_suggestions_feedback[i]["why"]
+
+ if 'relevant_lines_start' not in suggestion:
+ relevant_lines_start = code_suggestions_feedback[i].get('relevant_lines_start', -1)
+ relevant_lines_end = code_suggestions_feedback[i].get('relevant_lines_end', -1)
+ suggestion['relevant_lines_start'] = relevant_lines_start
+ suggestion['relevant_lines_end'] = relevant_lines_end
+ if relevant_lines_start < 0 or relevant_lines_end < 0:
+ suggestion["score"] = 0
+
+ try:
+ if get_settings().config.publish_output:
+ suggestion_statistics_dict = {'score': int(suggestion["score"]),
+ 'label': suggestion["label"].lower().strip()}
+ get_logger().info(f"PR-Agent suggestions statistics",
+ statistics=suggestion_statistics_dict, analytics=True)
+ except Exception as e:
+ get_logger().error(f"Failed to log suggestion statistics, error: {e}")
+ pass
+
+ except Exception as e: #
+ get_logger().error(f"Error processing suggestion score {i}",
+ artifact={"suggestion": suggestion,
+ "code_suggestions_feedback": code_suggestions_feedback[i]})
+ suggestion["score"] = 7
+ suggestion["score_why"] = ""
+
+ # if the before and after code is the same, clear one of them
+ try:
+ if suggestion['existing_code'] == suggestion['improved_code']:
+ get_logger().debug(
+ f"edited improved suggestion {i + 1}, because equal to existing code: {suggestion['existing_code']}")
+ if get_settings().pr_code_suggestions.commitable_code_suggestions:
+ suggestion['improved_code'] = "" # we need 'existing_code' to locate the code in the PR
+ else:
+ suggestion['existing_code'] = ""
+ except Exception as e:
+ get_logger().error(f"Error processing suggestion {i + 1}, error: {e}")
+ else:
+ # get_logger().error(f"Could not self-reflect on suggestions. using default score 7")
+ for i, suggestion in enumerate(data["code_suggestions"]):
+ suggestion["score"] = 7
+ suggestion["score_why"] = ""
return data
@@ -385,10 +427,10 @@ class PRCodeSuggestions:
suggestion_truncation_message = get_settings().get("PR_CODE_SUGGESTIONS.SUGGESTION_TRUNCATION_MESSAGE", "")
if max_code_suggestion_length > 0:
if len(suggestion['improved_code']) > max_code_suggestion_length:
- suggestion['improved_code'] = suggestion['improved_code'][:max_code_suggestion_length]
- suggestion['improved_code'] += f"\n{suggestion_truncation_message}"
get_logger().info(f"Truncated suggestion from {len(suggestion['improved_code'])} "
f"characters to {max_code_suggestion_length} characters")
+ suggestion['improved_code'] = suggestion['improved_code'][:max_code_suggestion_length]
+ suggestion['improved_code'] += f"\n{suggestion_truncation_message}"
return suggestion
def _prepare_pr_code_suggestions(self, predictions: str) -> Dict:
@@ -403,8 +445,7 @@ class PRCodeSuggestions:
one_sentence_summary_list = []
for i, suggestion in enumerate(data['code_suggestions']):
try:
- needed_keys = ['one_sentence_summary', 'label', 'relevant_file', 'relevant_lines_start',
- 'relevant_lines_end']
+ needed_keys = ['one_sentence_summary', 'label', 'relevant_file']
is_valid_keys = True
for key in needed_keys:
if key not in suggestion:
@@ -415,6 +456,11 @@ class PRCodeSuggestions:
if not is_valid_keys:
continue
+ if get_settings().get("pr_code_suggestions.focus_only_on_problems", False):
+ CRITICAL_LABEL = 'critical'
+ if CRITICAL_LABEL in suggestion['label'].lower(): # we want the published labels to be less declarative
+ suggestion['label'] = 'possible issue'
+
if suggestion['one_sentence_summary'] in one_sentence_summary_list:
get_logger().debug(f"Skipping suggestion {i + 1}, because it is a duplicate: {suggestion}")
continue
@@ -426,13 +472,6 @@ class PRCodeSuggestions:
continue
if ('existing_code' in suggestion) and ('improved_code' in suggestion):
- if suggestion['existing_code'] == suggestion['improved_code']:
- get_logger().debug(
- f"edited improved suggestion {i + 1}, because equal to existing code: {suggestion['existing_code']}")
- if get_settings().pr_code_suggestions.commitable_code_suggestions:
- suggestion['improved_code'] = "" # we need 'existing_code' to locate the code in the PR
- else:
- suggestion['existing_code'] = ""
suggestion = self._truncate_if_needed(suggestion)
one_sentence_summary_list.append(suggestion['one_sentence_summary'])
suggestion_list.append(suggestion)
@@ -535,9 +574,33 @@ class PRCodeSuggestions:
return True
return False
+ def remove_line_numbers(self, patches_diff_list: List[str]) -> List[str]:
+ # create a copy of the patches_diff_list, without line numbers for '__new hunk__' sections
+ try:
+ self.patches_diff_list_no_line_numbers = []
+ for patches_diff in self.patches_diff_list:
+ patches_diff_lines = patches_diff.splitlines()
+ for i, line in enumerate(patches_diff_lines):
+ if line.strip():
+ if line[0].isdigit():
+ # find the first letter in the line that starts with a valid letter
+ for j, char in enumerate(line):
+ if not char.isdigit():
+ patches_diff_lines[i] = line[j + 1:]
+ break
+ self.patches_diff_list_no_line_numbers.append('\n'.join(patches_diff_lines))
+ return self.patches_diff_list_no_line_numbers
+ except Exception as e:
+ get_logger().error(f"Error removing line numbers from patches_diff_list, error: {e}")
+ return patches_diff_list
+
async def _prepare_prediction_extended(self, model: str) -> dict:
self.patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
+
+ # create a copy of the patches_diff_list, without line numbers for '__new hunk__' sections
+ self.patches_diff_list_no_line_numbers = self.remove_line_numbers(self.patches_diff_list)
+
if self.patches_diff_list:
get_logger().info(f"Number of PR chunk calls: {len(self.patches_diff_list)}")
get_logger().debug(f"PR diff:", artifact=self.patches_diff_list)
@@ -545,12 +608,14 @@ class PRCodeSuggestions:
# parallelize calls to AI:
if get_settings().pr_code_suggestions.parallel_calls:
prediction_list = await asyncio.gather(
- *[self._get_prediction(model, patches_diff) for patches_diff in self.patches_diff_list])
+ *[self._get_prediction(model, patches_diff, patches_diff_no_line_numbers) for
+ patches_diff, patches_diff_no_line_numbers in
+ zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers)])
self.prediction_list = prediction_list
else:
prediction_list = []
- for i, patches_diff in enumerate(self.patches_diff_list):
- prediction = await self._get_prediction(model, patches_diff)
+ for patches_diff, patches_diff_no_line_numbers in zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers):
+ prediction = await self._get_prediction(model, patches_diff, patches_diff_no_line_numbers)
prediction_list.append(prediction)
data = {"code_suggestions": []}
@@ -559,18 +624,16 @@ class PRCodeSuggestions:
score_threshold = max(1, int(get_settings().pr_code_suggestions.suggestions_score_threshold))
for i, prediction in enumerate(predictions["code_suggestions"]):
try:
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- score = int(prediction.get("score", 1))
- if score >= score_threshold:
- data["code_suggestions"].append(prediction)
- else:
- get_logger().info(
- f"Removing suggestions {i} from call {j}, because score is {score}, and score_threshold is {score_threshold}",
- artifact=prediction)
- else:
+ score = int(prediction.get("score", 1))
+ if score >= score_threshold:
data["code_suggestions"].append(prediction)
+ else:
+ get_logger().info(
+ f"Removing suggestions {i} from call {j}, because score is {score}, and score_threshold is {score_threshold}",
+ artifact=prediction)
except Exception as e:
- get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}")
+ get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}",
+ artifact={"prediction": prediction})
self.data = data
else:
get_logger().warning(f"Empty PR diff list")
@@ -621,7 +684,7 @@ class PRCodeSuggestions:
if get_settings().pr_code_suggestions.final_clip_factor != 1:
max_len = max(
len(data_sorted),
- get_settings().pr_code_suggestions.num_code_suggestions_per_chunk,
+ int(get_settings().pr_code_suggestions.num_code_suggestions_per_chunk),
)
new_len = int(0.5 + max_len * get_settings().pr_code_suggestions.final_clip_factor)
if new_len < len(data_sorted):
@@ -654,10 +717,7 @@ class PRCodeSuggestions:
header = f"Suggestion"
delta = 66
header += " " * delta
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- pr_body += f"""Category | {header} | Score |
"""
- else:
- pr_body += f"""Category | {header} |
"""
+ pr_body += f"""Category | {header} | Score |
"""
pr_body += """"""
suggestions_labels = dict()
# add all suggestions related to each label
@@ -668,12 +728,11 @@ class PRCodeSuggestions:
suggestions_labels[label].append(suggestion)
# sort suggestions_labels by the suggestion with the highest score
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- suggestions_labels = dict(
- sorted(suggestions_labels.items(), key=lambda x: max([s['score'] for s in x[1]]), reverse=True))
- # sort the suggestions inside each label group by score
- for label, suggestions in suggestions_labels.items():
- suggestions_labels[label] = sorted(suggestions, key=lambda x: x['score'], reverse=True)
+ suggestions_labels = dict(
+ sorted(suggestions_labels.items(), key=lambda x: max([s['score'] for s in x[1]]), reverse=True))
+ # sort the suggestions inside each label group by score
+ for label, suggestions in suggestions_labels.items():
+ suggestions_labels[label] = sorted(suggestions, key=lambda x: x['score'], reverse=True)
counter_suggestions = 0
for label, suggestions in suggestions_labels.items():
@@ -732,16 +791,14 @@ class PRCodeSuggestions:
{example_code.rstrip()}
"""
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- pr_body += f"Suggestion importance[1-10]: {suggestion['score']}
\n\n"
- pr_body += f"Why: {suggestion['score_why']}\n\n"
- pr_body += f" "
+ pr_body += f"Suggestion importance[1-10]: {suggestion['score']}
\n\n"
+ pr_body += f"Why: {suggestion['score_why']}\n\n"
+ pr_body += f" "
pr_body += f""
# # add another column for 'score'
- if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
- pr_body += f"{suggestion['score']}\n\n"
+ pr_body += f" | {suggestion['score']}\n\n"
pr_body += f" | "
counter_suggestions += 1
diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py
index 2c3610d7..e5cfea2f 100644
--- a/pr_agent/tools/pr_description.py
+++ b/pr_agent/tools/pr_description.py
@@ -506,7 +506,7 @@ extra_file_yaml =
pr_body += "\n"
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
- changes_walkthrough = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH}\n{changes_walkthrough}"
+ changes_walkthrough = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH.value}\n{changes_walkthrough}"
else:
# if the value is a list, join its items by comma
if isinstance(value, list):
diff --git a/pr_agent/tools/ticket_pr_compliance_check.py b/pr_agent/tools/ticket_pr_compliance_check.py
index dc760ed1..05cd64fe 100644
--- a/pr_agent/tools/ticket_pr_compliance_check.py
+++ b/pr_agent/tools/ticket_pr_compliance_check.py
@@ -108,7 +108,7 @@ async def extract_tickets(git_provider):
async def extract_and_cache_pr_tickets(git_provider, vars):
- if get_settings().get('config.require_ticket_analysis_review', False):
+ if not get_settings().get('pr_reviewer.require_ticket_analysis_review', False):
return
related_tickets = get_settings().get('related_tickets', [])
if not related_tickets:
diff --git a/requirements.txt b/requirements.txt
index 0adf66ca..afb6af68 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,10 @@
aiohttp==3.9.5
-anthropic[vertex]==0.37.1
+anthropic[vertex]==0.39.0
atlassian-python-api==3.41.4
azure-devops==7.1.0b3
azure-identity==1.15.0
boto3==1.33.6
+certifi==2024.8.30
dynaconf==3.2.4
fastapi==0.111.0
GitPython==3.1.41
@@ -11,17 +12,17 @@ google-cloud-aiplatform==1.38.0
google-generativeai==0.8.3
google-cloud-storage==2.10.0
Jinja2==3.1.2
-litellm==1.50.2
+litellm==1.52.0
loguru==0.7.2
msrest==0.7.1
-openai==1.52.1
+openai==1.54.1
pytest==7.4.0
PyGithub==1.59.*
PyYAML==6.0.1
python-gitlab==3.15.0
retry==0.9.2
starlette-context==0.3.6
-tiktoken==0.7.0
+tiktoken==0.8.0
ujson==5.8.0
uvicorn==0.22.0
tenacity==8.2.3