Compare commits

..

1 Commits

Author SHA1 Message Date
36f7a7b17a A commit :: with multiple colons :: 2023-08-11 14:03:33 +03:00
15 changed files with 30 additions and 132 deletions

View File

@ -92,7 +92,6 @@ pip install -r requirements.txt
``` ```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
chmod 600 pr_agent/settings/.secrets.toml
# Edit .secrets.toml file # Edit .secrets.toml file
``` ```
@ -129,7 +128,6 @@ Allowing you to automate the review process on your private or public repositori
- Pull requests: Read & write - Pull requests: Read & write
- Issue comment: Read & write - Issue comment: Read & write
- Metadata: Read-only - Metadata: Read-only
- Contents: Read-only
- Set the following events: - Set the following events:
- Issue comment - Issue comment
- Pull request - Pull request

View File

@ -79,7 +79,7 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
|-------|---------------------------------------------|:------:|:------:|:---------:| |-------|---------------------------------------------|:------:|:------:|:---------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | | | ⮑ Inline review | :white_check_mark: | :white_check_mark: | |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Ask | :white_check_mark: | :white_check_mark: | |
| | Auto-Description | :white_check_mark: | :white_check_mark: | | | | Auto-Description | :white_check_mark: | :white_check_mark: | |
| | Improve Code | :white_check_mark: | :white_check_mark: | | | | Improve Code | :white_check_mark: | :white_check_mark: | |
| | Reflect and Review | :white_check_mark: | | | | | Reflect and Review | :white_check_mark: | | |

View File

@ -29,6 +29,7 @@ class AiHandler:
self.azure = False self.azure = False
if get_settings().get("OPENAI.ORG", None): if get_settings().get("OPENAI.ORG", None):
litellm.organization = get_settings().openai.org litellm.organization = get_settings().openai.org
self.deployment_id = get_settings().get("OPENAI.DEPLOYMENT_ID", None)
if get_settings().get("OPENAI.API_TYPE", None): if get_settings().get("OPENAI.API_TYPE", None):
if get_settings().openai.api_type == "azure": if get_settings().openai.api_type == "azure":
self.azure = True self.azure = True
@ -46,13 +47,6 @@ class AiHandler:
except AttributeError as e: except AttributeError as e:
raise ValueError("OpenAI key is required") from e raise ValueError("OpenAI key is required") from e
@property
def deployment_id(self):
"""
Returns the deployment ID for the OpenAI API.
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError), @retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3)) tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, temperature: float, system: str, user: str): async def chat_completion(self, model: str, temperature: float, system: str, user: str):
@ -76,15 +70,9 @@ class AiHandler:
TryAgain: If there is an attribute error during OpenAI inference. TryAgain: If there is an attribute error during OpenAI inference.
""" """
try: try:
deployment_id = self.deployment_id
if get_settings().config.verbosity_level >= 2:
logging.debug(
f"Generating completion with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
)
response = await acompletion( response = await acompletion(
model=model, model=model,
deployment_id=deployment_id, deployment_id=self.deployment_id,
messages=[ messages=[
{"role": "system", "content": system}, {"role": "system", "content": system},
{"role": "user", "content": user} {"role": "user", "content": user}

View File

@ -208,45 +208,18 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
async def retry_with_fallback_models(f: Callable): async def retry_with_fallback_models(f: Callable):
all_models = _get_all_models()
all_deployments = _get_all_deployments(all_models)
# try each (model, deployment_id) pair until one is successful, otherwise raise exception
for i, (model, deployment_id) in enumerate(zip(all_models, all_deployments)):
try:
get_settings().set("openai.deployment_id", deployment_id)
return await f(model)
except Exception as e:
logging.warning(
f"Failed to generate prediction with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}: "
f"{traceback.format_exc()}"
)
if i == len(all_models) - 1: # If it's the last iteration
raise # Re-raise the last exception
def _get_all_models() -> List[str]:
model = get_settings().config.model model = get_settings().config.model
fallback_models = get_settings().config.fallback_models fallback_models = get_settings().config.fallback_models
if not isinstance(fallback_models, list): if not isinstance(fallback_models, list):
fallback_models = [m.strip() for m in fallback_models.split(",")] fallback_models = [fallback_models]
all_models = [model] + fallback_models all_models = [model] + fallback_models
return all_models for i, model in enumerate(all_models):
try:
return await f(model)
def _get_all_deployments(all_models: List[str]) -> List[str]: except Exception as e:
deployment_id = get_settings().get("openai.deployment_id", None) logging.warning(f"Failed to generate prediction with {model}: {traceback.format_exc()}")
fallback_deployments = get_settings().get("openai.fallback_deployments", []) if i == len(all_models) - 1: # If it's the last iteration
if not isinstance(fallback_deployments, list) and fallback_deployments: raise # Re-raise the last exception
fallback_deployments = [d.strip() for d in fallback_deployments.split(",")]
if fallback_deployments:
all_deployments = [deployment_id] + fallback_deployments
if len(all_deployments) < len(all_models):
raise ValueError(f"The number of deployments ({len(all_deployments)}) "
f"is less than the number of models ({len(all_models)})")
else:
all_deployments = [deployment_id] * len(all_models)
return all_deployments
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo], def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],

View File

@ -261,7 +261,7 @@ def update_settings_from_args(args: List[str]) -> List[str]:
def load_yaml(review_text: str) -> dict: def load_yaml(review_text: str) -> dict:
review_text = review_text.removeprefix('```yaml').rstrip('`') review_text = review_text.lstrip('```yaml').rstrip('`')
try: try:
data = yaml.load(review_text, Loader=yaml.SafeLoader) data = yaml.load(review_text, Loader=yaml.SafeLoader)
except Exception as e: except Exception as e:

View File

@ -26,13 +26,6 @@ class BitbucketProvider:
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
def get_repo_settings(self):
try:
contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
return contents
except Exception:
return ""
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']:
return False return False
@ -100,13 +93,6 @@ class BitbucketProvider:
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError("Bitbucket provider does not support issue comments yet") raise NotImplementedError("Bitbucket provider does not support issue comments yet")
def get_repo_settings(self):
try:
contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
return contents
except Exception:
return ""
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
return True return True
@ -118,7 +104,7 @@ class BitbucketProvider:
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_url)
if 'bitbucket.org' not in parsed_url.netloc: if 'bitbucket.org' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid Bitbucket URL") raise ValueError("The provided URL is not a valid GitHub URL")
path_parts = parsed_url.path.strip('/').split('/') path_parts = parsed_url.path.strip('/').split('/')

View File

@ -89,10 +89,6 @@ class GitProvider(ABC):
def get_issue_comments(self): def get_issue_comments(self):
pass pass
@abstractmethod
def get_repo_settings(self):
pass
@abstractmethod @abstractmethod
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
pass pass

View File

@ -14,9 +14,6 @@ from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
logger = logging.getLogger() logger = logging.getLogger()
class DiffNotFoundError(Exception):
"""Raised when the diff for a merge request cannot be found."""
pass
class GitLabProvider(GitProvider): class GitLabProvider(GitProvider):
@ -59,7 +56,7 @@ class GitLabProvider(GitProvider):
self.last_diff = self.mr.diffs.list(get_all=True)[-1] self.last_diff = self.mr.diffs.list(get_all=True)[-1]
except IndexError as e: except IndexError as e:
logger.error(f"Could not get diff for merge request {self.id_mr}") logger.error(f"Could not get diff for merge request {self.id_mr}")
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e raise ValueError(f"Could not get diff for merge request {self.id_mr}") from e
def _get_pr_file_content(self, file_path: str, branch: str) -> str: def _get_pr_file_content(self, file_path: str, branch: str) -> str:
@ -153,20 +150,16 @@ class GitLabProvider(GitProvider):
def create_inline_comments(self, comments: list[dict]): def create_inline_comments(self, comments: list[dict]):
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet") raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int, def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
source_line_no: int, target_file: str,target_line_no: int) -> None: target_file, target_line_no):
if not found: if not found:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
else: else:
# in order to have exact sha's we have to find correct diff for this change d = self.last_diff
diff = self.get_relevant_diff(relevant_file, relevant_line_in_file)
if diff is None:
logger.error(f"Could not get diff for merge request {self.id_mr}")
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}")
pos_obj = {'position_type': 'text', pos_obj = {'position_type': 'text',
'new_path': target_file.filename, 'new_path': target_file.filename,
'old_path': target_file.old_filename if target_file.old_filename else target_file.filename, 'old_path': target_file.old_filename if target_file.old_filename else target_file.filename,
'base_sha': diff.base_commit_sha, 'start_sha': diff.start_commit_sha, 'head_sha': diff.head_commit_sha} 'base_sha': d.base_commit_sha, 'start_sha': d.start_commit_sha, 'head_sha': d.head_commit_sha}
if edit_type == 'deletion': if edit_type == 'deletion':
pos_obj['old_line'] = source_line_no - 1 pos_obj['old_line'] = source_line_no - 1
elif edit_type == 'addition': elif edit_type == 'addition':
@ -178,23 +171,6 @@ class GitLabProvider(GitProvider):
self.mr.discussions.create({'body': body, self.mr.discussions.create({'body': body,
'position': pos_obj}) 'position': pos_obj})
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
if not changes:
logging.error('No changes found for the merge request.')
return None
all_diffs = self.mr.diffs.list(get_all=True)
if not all_diffs:
logging.error('No diffs found for the merge request.')
return None
for diff in all_diffs:
for change in changes['changes']:
if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']:
return diff
logging.debug(
f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.')
return self.last_diff # fallback to last_diff if no relevant diff is found
def publish_code_suggestions(self, code_suggestions: list): def publish_code_suggestions(self, code_suggestions: list):
for suggestion in code_suggestions: for suggestion in code_suggestions:
try: try:

View File

@ -2,9 +2,8 @@ commands_text = "> **/review [-i]**: Request a review of your Pull Request. For
"considers changes since the last review, include the '-i' option.\n" \ "considers changes since the last review, include the '-i' option.\n" \
"> **/describe**: Modify the PR title and description based on the contents of the PR.\n" \ "> **/describe**: Modify the PR title and description based on the contents of the PR.\n" \
"> **/improve**: Suggest improvements to the code in the PR. \n" \ "> **/improve**: Suggest improvements to the code in the PR. \n" \
"> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n" \ "> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n\n" \
"> **/update_changelog**: Update the changelog based on the PR's contents.\n\n" \ ">To edit any configuration parameter from 'configuration.toml', add --config_path=new_value\n" \
">To edit any configuration parameter from **configuration.toml**, add --config_path=new_value\n" \
">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \ ">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \
">To list the possible configuration parameters, use the **/config** command.\n" \ ">To list the possible configuration parameters, use the **/config** command.\n" \

View File

@ -14,7 +14,6 @@ key = "" # Acquire through https://platform.openai.com
#api_version = '2023-05-15' # Check Azure documentation for the current API version #api_version = '2023-05-15' # Check Azure documentation for the current API version
#api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<your resource name>.openai.azure.com" #api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<your resource name>.openai.azure.com"
#deployment_id = "" # The deployment name you chose when you deployed the engine #deployment_id = "" # The deployment name you chose when you deployed the engine
#fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id
[anthropic] [anthropic]
key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/ key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/

View File

@ -3,7 +3,7 @@ system="""You are CodiumAI-PR-Reviewer, a language model designed to review git
Your task is to provide full description of the PR content. Your task is to provide full description of the PR content.
- Make sure not to focus the new PR code (the '+' lines). - Make sure not to focus the new PR code (the '+' lines).
- Notice that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or not up-to-date. Hence, compare them to the PR diff code, and use them only as a reference. - Notice that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or not up-to-date. Hence, compare them to the PR diff code, and use them only as a reference.
- If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
@ -33,7 +33,7 @@ PR Description:
PR Main Files Walkthrough: PR Main Files Walkthrough:
type: array type: array
maxItems: 10 maxItems: 10
description: |- description: >-
a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files). a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files).
items: items:
filename: filename:
@ -46,12 +46,10 @@ PR Main Files Walkthrough:
Example output: Example output:
```yaml ```yaml
PR Title: |- PR Title: ...
...
PR Type: PR Type:
- Bug fix - Bug fix
PR Description: |- PR Description: ...
...
PR Main Files Walkthrough: PR Main Files Walkthrough:
- ... - ...
- ... - ...

View File

@ -7,7 +7,6 @@ Your task is to provide constructive and concise feedback for the PR, and also p
- Suggestions should focus on improving the new added code lines. - Suggestions should focus on improving the new added code lines.
- Make sure not to provide suggestions repeating modifications already implemented in the new PR code (the '+' lines). - Make sure not to provide suggestions repeating modifications already implemented in the new PR code (the '+' lines).
{%- endif %} {%- endif %}
- If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %} {%- if extra_instructions %}
@ -79,7 +78,7 @@ PR Feedback:
description: the relevant file full path description: the relevant file full path
suggestion: suggestion:
type: string type: string
description: | description: >-
a concrete suggestion for meaningfully improving the new PR code. Also a concrete suggestion for meaningfully improving the new PR code. Also
describe how, specifically, the suggestion can be applied to new PR describe how, specifically, the suggestion can be applied to new PR
code. Add tags with importance measure that matches each suggestion code. Add tags with importance measure that matches each suggestion
@ -87,10 +86,10 @@ PR Feedback:
adding docstrings, renaming PR title and description, or linter like. adding docstrings, renaming PR title and description, or linter like.
relevant line: relevant line:
type: string type: string
description: | description: >-
a single code line taken from the relevant file, to which the suggestion applies. a single code line taken from the relevant file, to which the
The line should be a '+' line. suggestion applies. The line should be a '+' line. Make sure to output
Make sure to output the line exactly as it appears in the relevant file the line exactly as it appears in the relevant file
{%- endif %} {%- endif %}
{%- if require_security %} {%- if require_security %}
Security concerns: Security concerns:

View File

@ -93,10 +93,6 @@ class PRCodeSuggestions:
def push_inline_code_suggestions(self, data): def push_inline_code_suggestions(self, data):
code_suggestions = [] code_suggestions = []
if not data['Code suggestions']:
return self.git_provider.publish_comment('No suggestions found to improve this PR.')
for d in data['Code suggestions']: for d in data['Code suggestions']:
try: try:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:

View File

@ -237,7 +237,7 @@ class PRReviewer:
return return
review_text = self.prediction.strip() review_text = self.prediction.strip()
review_text = review_text.removeprefix('```yaml').rstrip('`') review_text = review_text.lstrip('```yaml').rstrip('`')
try: try:
data = yaml.load(review_text, Loader=SafeLoader) data = yaml.load(review_text, Loader=SafeLoader)
except Exception as e: except Exception as e:

View File

@ -1,10 +0,0 @@
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
class TestBitbucketProvider:
def test_parse_pr_url(self):
url = "https://bitbucket.org/WORKSPACE_XYZ/MY_TEST_REPO/pull-requests/321"
workspace_slug, repo_slug, pr_number = BitbucketProvider._parse_pr_url(url)
assert workspace_slug == "WORKSPACE_XYZ"
assert repo_slug == "MY_TEST_REPO"
assert pr_number == 321