Compare commits

..

1 Commits

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

View File

@ -18,18 +18,6 @@ docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> c
``` ```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>" docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>"
``` ```
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest.
The digest is a unique identifier for a specific version of an image. You can pull and run an image using its digest by referencing it like so: repository@sha256:digest. Always ensure you're using the correct and trusted digest for your operations.
1. To request a review for a PR using a specific digest, run the following command:
```bash
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
```
2. To ask a question about a PR using the same digest, run the following command:
```bash
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> ask "<your question>"
```
Possible questions you can ask include: Possible questions you can ask include:
@ -63,24 +51,7 @@ jobs:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
** if you want to pin your action to a specific commit for stability reasons
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@<commit_sha>
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
2. Add the following secret to your repository under `Settings > Secrets`: 2. Add the following secret to your repository under `Settings > Secrets`:
``` ```
@ -121,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
``` ```
@ -158,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

@ -15,7 +15,6 @@ from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
from pr_agent.tools.pr_config import PRConfig from pr_agent.tools.pr_config import PRConfig
command2class = { command2class = {
"auto_review": PRReviewer,
"answer": PRReviewer, "answer": PRReviewer,
"review": PRReviewer, "review": PRReviewer,
"review_pr": PRReviewer, "review_pr": PRReviewer,
@ -71,8 +70,6 @@ class PRAgent:
if notify: if notify:
notify() notify()
await PRReviewer(pr_url, is_answer=True, args=args).run() await PRReviewer(pr_url, is_answer=True, args=args).run()
elif action == "auto_review":
await PRReviewer(pr_url, is_auto=True, args=args).run()
elif action in command2class: elif action in command2class:
if notify: if notify:
notify() notify()

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

@ -245,12 +245,14 @@ def update_settings_from_args(args: List[str]) -> List[str]:
arg = arg.strip() arg = arg.strip()
if arg.startswith('--'): if arg.startswith('--'):
arg = arg.strip('-').strip() arg = arg.strip('-').strip()
vals = arg.split('=', 1) vals = arg.split('=')
if len(vals) != 2: if len(vals) != 2:
logging.error(f'Invalid argument format: {arg}') logging.error(f'Invalid argument format: {arg}')
other_args.append(arg) other_args.append(arg)
continue continue
key, value = _fix_key_value(*vals) key, value = vals
key = key.strip().upper()
value = value.strip()
get_settings().set(key, value) get_settings().set(key, value)
logging.info(f'Updated setting {key} to: "{value}"') logging.info(f'Updated setting {key} to: "{value}"')
else: else:
@ -258,18 +260,8 @@ def update_settings_from_args(args: List[str]) -> List[str]:
return other_args return other_args
def _fix_key_value(key: str, value: str):
key = key.strip().upper()
value = value.strip()
try:
value = yaml.safe_load(value)
except Exception as e:
logging.error(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
return key, value
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

@ -93,7 +93,7 @@ async def handle_request(body: Dict[str, Any]):
api_url = pull_request.get("url") api_url = pull_request.get("url")
if not api_url: if not api_url:
return {} return {}
await agent.handle_request(api_url, "/auto_review") await agent.handle_request(api_url, "/review")
return {} return {}

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

@ -19,7 +19,6 @@ require_security_review=true
num_code_suggestions=3 num_code_suggestions=3
inline_code_comments = false inline_code_comments = false
ask_and_reflect=false ask_and_reflect=false
automatic_review=true
extra_instructions = "" extra_instructions = ""
[pr_description] # /describe # [pr_description] # /describe #

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

@ -23,7 +23,7 @@ class PRReviewer:
""" """
The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model. The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model.
""" """
def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False, args: list = None): def __init__(self, pr_url: str, is_answer: bool = False, args: list = None):
""" """
Initialize the PRReviewer object with the necessary attributes and objects to review a pull request. Initialize the PRReviewer object with the necessary attributes and objects to review a pull request.
@ -40,7 +40,6 @@ class PRReviewer:
) )
self.pr_url = pr_url self.pr_url = pr_url
self.is_answer = is_answer self.is_answer = is_answer
self.is_auto = is_auto
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"): if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now") raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
@ -94,12 +93,8 @@ class PRReviewer:
""" """
Review the pull request and generate feedback. Review the pull request and generate feedback.
""" """
if self.is_auto and not get_settings().pr_reviewer.automatic_review: logging.info('Reviewing PR...')
logging.info(f'Automatic review is disabled {self.pr_url}')
return None
logging.info(f'Reviewing PR: {self.pr_url} ...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
@ -242,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

@ -14,4 +14,4 @@ GitPython~=3.1.32
litellm~=0.1.351 litellm~=0.1.351
PyYAML==6.0 PyYAML==6.0
starlette-context==0.3.6 starlette-context==0.3.6
litellm~=0.1.351 litellm~=0.1.351

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