Compare commits

..

60 Commits

Author SHA1 Message Date
9770f4709a few more changes suggested by AI implemented 2023-08-19 16:26:15 +03:00
35afe758e9 revert back conf 2023-08-19 16:16:16 +03:00
50125ae57f various changes as outcomes from AI review 2023-08-19 16:12:48 +03:00
6595c3e0c9 2 more /improve good suggestions 2023-08-19 15:47:45 +03:00
fdd16f6c75 raize exception when no diffs in MR 2023-08-19 15:40:40 +03:00
7b7e913195 to changes suggested by /improve with my small touch 2023-08-19 15:31:02 +03:00
5477469a91 in order to have exact sha's we have to find correct diff for this change
otherwise gitlab web doesn't able show diff on page and return 500 or 400 errors based on different scenarios
2023-08-19 15:06:22 +03:00
dee1f168f8 Merge pull request #206 from pzarfos/fix/bitbucket_get_repo_settings
Implement get_repo_settings for BitbucketProvider
2023-08-16 18:54:36 +03:00
bb18e32c56 Merge pull request #209 from tjwp/no-suggestions
Publish comment when improve has no suggestions
2023-08-16 18:53:39 +03:00
70286e9574 Make the message more modest 2023-08-15 08:35:57 -04:00
3f60d12a9a Publish comment when improve has no suggestions 2023-08-14 13:07:00 -04:00
164b340c29 Merge branch 'main' into fix/bitbucket_get_repo_settings 2023-08-14 08:30:14 -04:00
4bb035ec0f Merge pull request #208 from sarbjitsinghgrewal/fix_bitbucket_ask_issue
Fix bitbucket ask issue
2023-08-14 14:52:45 +03:00
23a79bc8fe Merge pull request #183 from zmeir/zmeir-fallback_deployments
Support fallback deployments to accompany fallback models
2023-08-14 14:51:14 +03:00
1db53ae1ad update readme file 2023-08-14 14:45:25 +05:30
cca951d787 fix bitbucket ask issue 2023-08-14 14:30:30 +05:30
230d684cd3 Merge pull request #202 from zmeir/zmeir-remove_blank_line
Remove extra blank line in help message
2023-08-14 07:12:50 +03:00
0a02fa8597 Merge pull request #203 from tjwp/tjwp/contents-permission
Contents read-only permission needed by /review -i
2023-08-14 07:12:38 +03:00
f82b9620af Implement get_repo_settings for BitbucketProvider 2023-08-13 18:25:11 -04:00
ce29d9eb49 Contents read-only permission needed by /review -i 2023-08-13 07:28:05 -04:00
b7b650eb05 Remove extra blank line in help message 2023-08-13 11:32:02 +03:00
6ca0655517 Extracted to helper functions 2023-08-13 11:03:10 +03:00
edcf89a456 Improve comment 2023-08-13 10:56:16 +03:00
7762a67250 Fail if not enough fallback deployments 2023-08-13 10:55:44 +03:00
7049c73790 Merge branch 'main' into zmeir-fallback_deployments 2023-08-13 10:48:21 +03:00
cc7be0811a Merge pull request #200 from Codium-ai/tr/block_scalar
Block scalar format
2023-08-12 09:49:27 +03:00
d3a5aea89e update_changelog 2023-08-11 18:50:56 +03:00
dd87df49f5 block scalar 2023-08-11 18:43:46 +03:00
e85bcf3a17 Merge remote-tracking branch 'origin/tr/block_scalar' into tr/block_scalar 2023-08-11 18:38:06 +03:00
abb754b16b block scalar 2023-08-11 18:37:55 +03:00
bb5878c99a Merge branch 'main' into tr/block_scalar 2023-08-11 18:36:21 +03:00
273a9e35d9 block scalar 2023-08-11 18:35:34 +03:00
fcc208d09f Merge pull request #194 from Codium-ai/ok/test_action
Implementing GitHub Actions Workflow for Build and Test
2023-08-10 16:47:26 +03:00
20bbdac135 Test github action 2023-08-10 16:41:50 +03:00
ceedf2bf83 Merge branch 'main' into ok/test_action 2023-08-10 16:40:01 +03:00
2d6b947292 Test github action 2023-08-10 16:37:02 +03:00
2e13b12fe6 Merge pull request #193 from coditamar/fix/workflow_yaml_permissions
adding `permissions` to `review.yaml`, also adding some comments
2023-08-10 16:17:32 +03:00
2d56c88291 Merge remote-tracking branch 'upstream/main' into fix/workflow_yaml_permissions 2023-08-10 16:16:47 +03:00
cf9c6a872d Test github action 2023-08-10 16:09:29 +03:00
0bb8ab70a4 Merge remote-tracking branch 'origin/main' 2023-08-10 15:16:10 +03:00
4a47b78a90 Rename workflow 2023-08-10 15:16:03 +03:00
3e542cd88b adding permissions to review.yaml, also adding some comments 2023-08-10 08:10:10 +03:00
17ed050ca7 Merge pull request #192 from coditamar/fix/minor_cli_and_requirements_fixes
Correcting CLI and README Descriptions and Fixing Requirements.txt
2023-08-10 02:18:13 +03:00
e24c5e3501 Update requirements.txt 2023-08-10 02:16:16 +03:00
b206b1c5ff Protect for empty description 2023-08-10 02:08:36 +03:00
0270306d3c litellm was mentioned twice in the requirements.txt 2023-08-10 01:34:24 +03:00
3e09b9ac37 fixing pr_url param description (was wrongly mentioned as pr-url) 2023-08-10 01:31:06 +03:00
725ac9e85d fixing cli pr_url help description 2023-08-10 01:30:12 +03:00
e00500b90c PyYAML dependency 2023-08-10 00:56:28 +03:00
f1f271fa00 PyYAML dependency 2023-08-10 00:44:00 +03:00
d38c5236dd Merge pull request #187 from Codium-ai/ok/limit_description
Limiting Description and Commit Messages Length
2023-08-09 14:14:47 +03:00
49a3a1e511 Merge pull request #188 from Codium-ai/tr/update_review_prompt
Update PR Review and Description Generation to Use YAML
2023-08-09 14:14:36 +03:00
1b0b90e51d block scalar 2023-08-09 14:11:58 +03:00
64481e2d84 block scalar 2023-08-09 14:01:48 +03:00
fe75e3f2ec yaml
yaml
2023-08-09 12:15:52 +03:00
95b6abef09 Merge branch 'main' into zmeir-fallback_deployments 2023-08-08 11:00:13 +03:00
7f1849a867 Logging 2023-08-07 22:42:53 +03:00
7760f37dee Merge pull request #185 from zmeir/zmeir-fix_inline_comment_position
Attempt to fix bug in create_inline_comment
2023-08-07 20:41:52 +03:00
164ed77d72 Attempt to fix bug in create_inline_comment 2023-08-07 17:09:50 +03:00
6c4a5bae52 Support fallback deployments to accompany fallback models
This is useful for example in Azure OpenAI deployments where you have a different deployment per model, so the current fallback implementation doesn't work (still uses the same deployment for each fallback attempt)
2023-08-07 16:18:48 +03:00
24 changed files with 450 additions and 180 deletions

36
.github/workflows/build-and-test.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Build-and-test
on:
push:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- id: checkout
uses: actions/checkout@v2
- id: dockerx
name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- id: build
name: Build dev docker
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
push: false
load: true
tags: codiumai/pr-agent:test
cache-from: type=gha,scope=dev
cache-to: type=gha,mode=max,scope=dev
target: test
- id: test
name: Test dev docker
run: |
docker run --rm codiumai/pr-agent:test pytest -v

View File

@ -1,6 +1,17 @@
# This workflow enables developers to call PR-Agents `/[actions]` in PR's comments and upon PR creation.
# Learn more at https://www.codium.ai/pr-agent/
# This is v0.2 of this workflow file
name: PR-Agent
on:
pull_request:
issue_comment:
permissions:
issues: write
pull-requests: write
jobs:
pr_agent_job:
runs-on: ubuntu-latest

View File

@ -92,6 +92,7 @@ pip install -r requirements.txt
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
chmod 600 pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
@ -128,6 +129,7 @@ Allowing you to automate the review process on your private or public repositori
- Pull requests: Read & write
- Issue comment: Read & write
- Metadata: Read-only
- Contents: Read-only
- Set the following events:
- Issue comment
- 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: |
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | |
| | Ask | :white_check_mark: | :white_check_mark: | |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark:
| | Auto-Description | :white_check_mark: | :white_check_mark: | |
| | Improve Code | :white_check_mark: | :white_check_mark: | |
| | Reflect and Review | :white_check_mark: | | |
@ -97,12 +97,12 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
| | Incremental PR Review | :white_check_mark: | | |
Examples for invoking the different tools via the CLI:
- **Review**: python cli.py --pr-url=<pr_url> review
- **Describe**: python cli.py --pr-url=<pr_url> describe
- **Improve**: python cli.py --pr-url=<pr_url> improve
- **Ask**: python cli.py --pr-url=<pr_url> ask "Write me a poem about this PR"
- **Reflect**: python cli.py --pr-url=<pr_url> reflect
- **Update Changelog**: python cli.py --pr-url=<pr_url> update_changelog
- **Review**: python cli.py --pr_url=<pr_url> review
- **Describe**: python cli.py --pr_url=<pr_url> describe
- **Improve**: python cli.py --pr_url=<pr_url> improve
- **Ask**: python cli.py --pr_url=<pr_url> ask "Write me a poem about this PR"
- **Reflect**: python cli.py --pr_url=<pr_url> reflect
- **Update Changelog**: python cli.py --pr_url=<pr_url> update_changelog
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).

View File

@ -4,17 +4,21 @@ WORKDIR /app
ADD pyproject.toml .
RUN pip install . && rm pyproject.toml
ENV PYTHONPATH=/app
ADD pr_agent pr_agent
FROM base as github_app
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_app.py"]
FROM base as github_polling
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"]
FROM base as test
ADD requirements-dev.txt .
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt
ADD pr_agent pr_agent
ADD tests tests
FROM base as cli
ADD pr_agent pr_agent
ENTRYPOINT ["python", "pr_agent/cli.py"]

View File

@ -29,7 +29,6 @@ class AiHandler:
self.azure = False
if get_settings().get("OPENAI.ORG", None):
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().openai.api_type == "azure":
self.azure = True
@ -47,6 +46,13 @@ class AiHandler:
except AttributeError as 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),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, temperature: float, system: str, user: str):
@ -70,9 +76,15 @@ class AiHandler:
TryAgain: If there is an attribute error during OpenAI inference.
"""
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(
model=model,
deployment_id=self.deployment_id,
deployment_id=deployment_id,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}

View File

@ -208,18 +208,45 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
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
fallback_models = get_settings().config.fallback_models
if not isinstance(fallback_models, list):
fallback_models = [fallback_models]
fallback_models = [m.strip() for m in fallback_models.split(",")]
all_models = [model] + fallback_models
for i, model in enumerate(all_models):
try:
return await f(model)
except Exception as e:
logging.warning(f"Failed to generate prediction with {model}: {traceback.format_exc()}")
if i == len(all_models) - 1: # If it's the last iteration
raise # Re-raise the last exception
return all_models
def _get_all_deployments(all_models: List[str]) -> List[str]:
deployment_id = get_settings().get("openai.deployment_id", None)
fallback_deployments = get_settings().get("openai.fallback_deployments", [])
if not isinstance(fallback_deployments, list) and fallback_deployments:
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],
@ -298,12 +325,16 @@ def clip_tokens(text: str, max_tokens: int) -> str:
str: The clipped string.
"""
# We'll estimate the number of tokens by hueristically assuming 2.5 tokens per word
encoder = get_token_encoder()
num_input_tokens = len(encoder.encode(text))
if num_input_tokens <= max_tokens:
return text
num_chars = len(text)
chars_per_token = num_chars / num_input_tokens
num_output_chars = int(chars_per_token * max_tokens)
clipped_text = text[:num_output_chars]
return clipped_text
try:
encoder = get_token_encoder()
num_input_tokens = len(encoder.encode(text))
if num_input_tokens <= max_tokens:
return text
num_chars = len(text)
chars_per_token = num_chars / num_input_tokens
num_output_chars = int(chars_per_token * max_tokens)
clipped_text = text[:num_output_chars]
return clipped_text
except Exception as e:
logging.warning(f"Failed to clip tokens: {e}")
return text

View File

@ -8,8 +8,8 @@ import textwrap
from datetime import datetime
from typing import Any, List
import yaml
from starlette_context import context
from pr_agent.config_loader import get_settings, global_settings
@ -258,3 +258,26 @@ def update_settings_from_args(args: List[str]) -> List[str]:
else:
other_args.append(arg)
return other_args
def load_yaml(review_text: str) -> dict:
review_text = review_text.removeprefix('```yaml').rstrip('`')
try:
data = yaml.load(review_text, Loader=yaml.SafeLoader)
except Exception as e:
logging.error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text)
return data
def try_fix_yaml(review_text: str) -> dict:
review_text_lines = review_text.split('\n')
data = {}
for i in range(1, len(review_text_lines)):
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
try:
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader)
logging.info(f"Successfully parsed AI prediction after removing {i} lines")
break
except:
pass
return data

View File

@ -10,13 +10,13 @@ from pr_agent.config_loader import get_settings
def run(inargs=None):
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
"""\
Usage: cli.py --pr-url <URL on supported git hosting service> <command> [<args>].
Usage: cli.py --pr-url=<URL on supported git hosting service> <command> [<args>].
For example:
- cli.py --pr-url=... review
- cli.py --pr-url=... describe
- cli.py --pr-url=... improve
- cli.py --pr-url=... ask "write me a poem about this PR"
- cli.py --pr-url=... reflect
- cli.py --pr_url=... review
- cli.py --pr_url=... describe
- cli.py --pr_url=... improve
- cli.py --pr_url=... ask "write me a poem about this PR"
- cli.py --pr_url=... reflect
Supported commands:
review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
@ -27,7 +27,7 @@ reflect - Ask the PR author questions about the PR.
update_changelog - Update the changelog based on the PR's contents.
To edit any configuration parameter from 'configuration.toml', just add -config_path=<value>.
For example: '- cli.py --pr-url=... review --pr_reviewer.extra_instructions="focus on the file: ..."'
For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions="focus on the file: ..."'
""")
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
parser.add_argument('command', type=str, help='The', choices=commands, default='review')

View File

@ -26,6 +26,13 @@ class BitbucketProvider:
if 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:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']:
return False
@ -93,6 +100,13 @@ class BitbucketProvider:
def get_issue_comments(self):
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]:
return True
@ -104,7 +118,7 @@ class BitbucketProvider:
parsed_url = urlparse(pr_url)
if 'bitbucket.org' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid GitHub URL")
raise ValueError("The provided URL is not a valid Bitbucket URL")
path_parts = parsed_url.path.strip('/').split('/')

View File

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

View File

@ -153,7 +153,7 @@ class GithubProvider(GitProvider):
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
if position == -1:
if get_settings().config.verbosity_level >= 2:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
@ -398,10 +398,13 @@ class GithubProvider(GitProvider):
def generate_link_to_relevant_line_number(self, suggestion) -> str:
try:
relevant_file = suggestion['relevant file']
relevant_file = suggestion['relevant file'].strip('`').strip("'")
relevant_line_str = suggestion['relevant line']
if not relevant_line_str:
return ""
position, absolute_position = find_line_number_of_relevant_line_in_file \
(self.diff_files, relevant_file.strip('`'), relevant_line_str)
(self.diff_files, relevant_file, relevant_line_str)
if absolute_position != -1:
# # link to right file only
@ -416,4 +419,4 @@ class GithubProvider(GitProvider):
if get_settings().config.verbosity_level >= 2:
logging.info(f"Failed adding line link, error: {e}")
return ""
return ""

View File

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

View File

@ -2,8 +2,9 @@ commands_text = "> **/review [-i]**: Request a review of your Pull Request. For
"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" \
"> **/improve**: Suggest improvements to the code in the PR. \n" \
"> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n\n" \
">To edit any configuration parameter from 'configuration.toml', add --config_path=new_value\n" \
"> **/ask \\<QUESTION\\>**: Pose a question about the PR.\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" \
">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \
">To list the possible configuration parameters, use the **/config** command.\n" \

View File

@ -14,6 +14,7 @@ key = "" # Acquire through https://platform.openai.com
#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"
#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]
key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/

View File

@ -2,38 +2,67 @@
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Your task is to provide full description of the PR content.
- 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.
- If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %}
Extra instructions from the user:
{{ extra_instructions }}
{% endif %}
You must use the following JSON schema to format your answer:
```json
{
"PR Title": {
"type": "string",
"description": "an informative title for the PR, describing its main theme"
},
"PR Type": {
"type": "string",
"description": possible values are: ["Bug fix", "Tests", "Bug fix with tests", "Refactoring", "Enhancement", "Documentation", "Other"]
},
"PR Description": {
"type": "string",
"description": "an informative and concise description of the PR"
},
"PR Main Files Walkthrough": {
"type": "string",
"description": "a walkthrough of the PR changes. Review main files, in bullet points, and shortly describe the changes in each file (up to 10 most important files). Format: -`filename`: description of changes\n..."
}
}
You must use the following YAML schema to format your answer:
```yaml
PR Title:
type: string
description: an informative title for the PR, describing its main theme
PR Type:
type: array
items:
type: string
enum:
- Bug fix
- Tests
- Bug fix with tests
- Refactoring
- Enhancement
- Documentation
- Other
PR Description:
type: string
description: an informative and concise description of the PR
PR Main Files Walkthrough:
type: array
maxItems: 10
description: |-
a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files).
items:
filename:
type: string
description: the relevant file full path
changes in file:
type: string
description: minimal and concise description of the changes in the relevant file
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
Example output:
```yaml
PR Title: |-
...
PR Type:
- Bug fix
PR Description: |-
...
PR Main Files Walkthrough:
- ...
- ...
```
Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
"""
user="""PR Info:
Previous title: '{{title}}'
Previous description: '{{description}}'
Branch: '{{branch}}'
{%- if language %}
@ -52,6 +81,6 @@ The PR Git Diff:
```
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines.
Response (should be a valid JSON, and nothing else):
```json
Response (should be a valid YAML, and nothing else):
```yaml
"""

View File

@ -7,6 +7,7 @@ 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.
- Make sure not to provide suggestions repeating modifications already implemented in the new PR code (the '+' lines).
{%- endif %}
- If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %}
@ -14,117 +15,121 @@ Extra instructions from the user:
{{ extra_instructions }}
{% endif %}
You must use the following JSON schema to format your answer:
```json
{
"PR Analysis": {
"Main theme": {
"type": "string",
"description": "a short explanation of the PR"
},
"Type of PR": {
"type": "string",
"enum": ["Bug fix", "Tests", "Refactoring", "Enhancement", "Documentation", "Other"]
},
You must use the following YAML schema to format your answer:
```yaml
PR Analysis:
Main theme:
type: string
description: a short explanation of the PR
Type of PR:
type: string
enum:
- Bug fix
- Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- if require_score %}
"Score": {
"type": "int",
"description": "Rate this PR on a scale of 0-100 (inclusive), where 0 means the worst possible PR code, and 100 means PR code of the highest quality, without any bugs or performance issues, that is ready to be merged immediately and run in production at scale."
},
Score:
type: int
description: >-
Rate this PR on a scale of 0-100 (inclusive), where 0 means the worst
possible PR code, and 100 means PR code of the highest quality, without
any bugs or performance issues, that is ready to be merged immediately and
run in production at scale.
{%- endif %}
{%- if require_tests %}
"Relevant tests added": {
"type": "string",
"description": "yes\\no question: does this PR have relevant tests ?"
},
Relevant tests added:
type: string
description: yes\\no question: does this PR have relevant tests ?
{%- endif %}
{%- if question_str %}
"Insights from user's answer": {
"type": "string",
"description": "shortly summarize the insights you gained from the user's answers to the questions"
},
Insights from user's answer:
type: string
description: >-
shortly summarize the insights you gained from the user's answers to the questions
{%- endif %}
{%- if require_focused %}
"Focused PR": {
"type": "string",
"description": "Is this a focused PR, in the sense that all the PR code diff changes are united under a single focused theme ? If the theme is too broad, or the PR code diff changes are too scattered, then the PR is not focused. Explain your answer shortly."
}
},
Focused PR:
type: string
description: >-
Is this a focused PR, in the sense that all the PR code diff changes are
united under a single focused theme ? If the theme is too broad, or the PR
code diff changes are too scattered, then the PR is not focused. Explain
your answer shortly.
{%- endif %}
"PR Feedback": {
"General suggestions": {
"type": "string",
"description": "General suggestions and feedback for the contributors and maintainers of this PR. May include important suggestions for the overall structure, primary purpose, best practices, critical bugs, and other aspects of the PR. Don't address PR title and description, or lack of tests. Explain your suggestions."
},
PR Feedback:
General suggestions:
type: string
description: >-
General suggestions and feedback for the contributors and maintainers of
this PR. May include important suggestions for the overall structure,
primary purpose, best practices, critical bugs, and other aspects of the
PR. Don't address PR title and description, or lack of tests. Explain your
suggestions.
{%- if num_code_suggestions > 0 %}
"Code feedback": {
"type": "array",
"maxItems": {{ num_code_suggestions }},
"uniqueItems": true,
"items": {
"relevant file": {
"type": "string",
"description": "the relevant file full path"
},
"suggestion": {
"type": "string",
"description": "a concrete suggestion for meaningfully improving the new PR code. Also describe how, specifically, the suggestion can be applied to new PR code. Add tags with importance measure that matches each suggestion ('important' or 'medium'). Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.
},
"relevant line": {
"type": "string",
"description": "a single code line taken from the relevant file, to which the suggestion applies. The line should be a '+' line. Make sure to output the line exactly as it appears in the relevant file"
}
}
},
Code feedback:
type: array
maxItems: {{ num_code_suggestions }}
uniqueItems: true
items:
relevant file:
type: string
description: the relevant file full path
suggestion:
type: string
description: |
a concrete suggestion for meaningfully improving the new PR code. Also
describe how, specifically, the suggestion can be applied to new PR
code. Add tags with importance measure that matches each suggestion
('important' or 'medium'). Do not make suggestions for updating or
adding docstrings, renaming PR title and description, or linter like.
relevant line:
type: string
description: |
a single code line taken from the relevant file, to which the suggestion applies.
The line should be a '+' line.
Make sure to output the line exactly as it appears in the relevant file
{%- endif %}
{%- if require_security %}
"Security concerns": {
"type": "string",
"description": "yes\\no question: does this PR code introduce possible security concerns or issues, like SQL injection, XSS, CSRF, and others ? If answered 'yes', explain your answer shortly"
? explain your answer shortly"
}
Security concerns:
type: string
description: >-
yes\\no question: does this PR code introduce possible security concerns or
issues, like SQL injection, XSS, CSRF, and others ? If answered 'yes',explain your answer shortly
{%- endif %}
}
}
```
Example output:
'
{
"PR Analysis":
{
"Main theme": "xxx",
"Type of PR": "Bug fix",
```yaml
PR Analysis:
Main theme: xxx
Type of PR: Bug fix
{%- if require_score %}
"Score": 89,
{%- endif %}
{%- if require_tests %}
"Relevant tests added": "No",
Score: 89
{%- endif %}
Relevant tests added: No
{%- if require_focused %}
"Focused PR": "yes\\no, because ..."
Focused PR: no, because ...
{%- endif %}
},
"PR Feedback":
{
"General PR suggestions": "..., `xxx`...",
PR Feedback:
General PR suggestions: ...
{%- if num_code_suggestions > 0 %}
"Code feedback": [
{
"relevant file": "directory/xxx.py",
"suggestion": "xxx [important]",
"relevant line": "xxx",
},
...
]
Code feedback:
- relevant file: |-
directory/xxx.py
suggestion: xxx [important]
relevant line: |-
xxx
...
{%- endif %}
{%- if require_security %}
"Security concerns": "No, because ..."
Security concerns: No
{%- endif %}
}
}
'
```
Make sure to output a valid YAML. Use multi-line block scalar ('|') if needed.
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
"""
@ -158,6 +163,6 @@ The PR Git Diff:
```
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines.
Response (should be a valid JSON, and nothing else):
```json
Response (should be a valid YAML, and nothing else):
```yaml
"""

View File

@ -93,6 +93,10 @@ class PRCodeSuggestions:
def push_inline_code_suggestions(self, data):
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']:
try:
if get_settings().config.verbosity_level >= 2:

View File

@ -8,6 +8,7 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
@ -139,34 +140,45 @@ class PRDescription:
- title: a string containing the PR title.
- pr_body: a string containing the PR body in a markdown format.
- pr_types: a list of strings containing the PR types.
- markdown_text: a string containing the AI prediction data in a markdown format.
- markdown_text: a string containing the AI prediction data in a markdown format. used for publishing a comment
"""
# Load the AI prediction data into a dictionary
data = json.loads(self.prediction)
data = load_yaml(self.prediction.strip())
# Initialization
markdown_text = pr_body = ""
pr_types = []
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = ""
for key, value in data.items():
markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n"
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'PR Type' in data:
pr_types = data['PR Type'].split(',')
if type(data['PR Type']) == list:
pr_types = data['PR Type']
elif type(data['PR Type']) == str:
pr_types = data['PR Type'].split(',')
# Assign the value of the 'PR Title' key to 'title' variable and remove it from the dictionary
title = data.pop('PR Title')
# Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format,
# except for the items containing the word 'walkthrough'
pr_body = ""
for key, value in data.items():
pr_body += f"## {key}:\n"
if 'walkthrough' in key.lower():
pr_body += f"{value}\n"
# for filename, description in value.items():
for file in value:
filename = file['filename'].replace("'", "`")
description = file['changes in file']
pr_body += f'`{filename}`: {description}\n'
else:
# if the value is a list, join its items by comma
if type(value) == list:
value = ', '.join(v for v in value)
pr_body += f"{value}\n\n___\n"
if get_settings().config.verbosity_level >= 2:

View File

@ -4,13 +4,15 @@ import logging
from collections import OrderedDict
from typing import List, Tuple
import yaml
from jinja2 import Environment, StrictUndefined
from yaml import SafeLoader
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, \
find_line_number_of_relevant_line_in_file, clip_tokens
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import convert_to_markdown, try_fix_json
from pr_agent.algo.utils import convert_to_markdown, try_fix_json, try_fix_yaml, load_yaml
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language
@ -160,19 +162,17 @@ class PRReviewer:
Prepare the PR review by processing the AI prediction and generating a markdown-formatted text that summarizes
the feedback.
"""
review = self.prediction.strip()
try:
data = json.loads(review)
except json.decoder.JSONDecodeError:
data = try_fix_json(review)
data = load_yaml(self.prediction.strip())
# Move 'Security concerns' key to 'PR Analysis' section for better display
pr_feedback = data.get('PR Feedback', {})
security_concerns = pr_feedback.get('Security concerns')
if security_concerns:
if security_concerns is not None:
del pr_feedback['Security concerns']
data.setdefault('PR Analysis', {})['Security concerns'] = security_concerns
if type(security_concerns) == bool and security_concerns == False:
data.setdefault('PR Analysis', {})['Security concerns'] = 'No security concerns found'
else:
data.setdefault('PR Analysis', {})['Security concerns'] = security_concerns
#
if 'Code feedback' in pr_feedback:
@ -183,6 +183,12 @@ class PRReviewer:
del pr_feedback['Code feedback']
else:
for suggestion in code_feedback:
if ('relevant file' in suggestion) and (not suggestion['relevant file'].startswith('``')):
suggestion['relevant file'] = f"``{suggestion['relevant file']}``"
if 'relevant line' not in suggestion:
suggestion['relevant line'] = ''
relevant_line_str = suggestion['relevant line'].split('\n')[0]
# removing '+'
@ -219,7 +225,7 @@ class PRReviewer:
logging.info(f"Markdown response:\n{markdown_text}")
if markdown_text == None or len(markdown_text) == 0:
markdown_text = review
markdown_text = ""
return markdown_text
@ -230,11 +236,13 @@ class PRReviewer:
if get_settings().pr_reviewer.num_code_suggestions == 0:
return
review = self.prediction.strip()
review_text = self.prediction.strip()
review_text = review_text.removeprefix('```yaml').rstrip('`')
try:
data = json.loads(review)
except json.decoder.JSONDecodeError:
data = try_fix_json(review)
data = yaml.load(review_text, Loader=SafeLoader)
except Exception as e:
logging.error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text)
comments: List[str] = []
for suggestion in data.get('PR Feedback', {}).get('Code feedback', []):

View File

@ -42,7 +42,8 @@ dependencies = [
"atlassian-python-api==3.39.0",
"GitPython~=3.1.32",
"starlette-context==0.3.6",
"litellm~=0.1.351"
"litellm~=0.1.351",
"PyYAML==6.0"
]
[project.urls]

View File

@ -11,4 +11,7 @@ pytest~=7.4.0
aiohttp~=3.8.4
atlassian-python-api==3.39.0
GitPython~=3.1.32
litellm~=0.1.351
PyYAML==6.0
starlette-context==0.3.6
litellm~=0.1.351

View File

@ -0,0 +1,10 @@
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

View File

@ -0,0 +1,32 @@
# Generated by CodiumAI
import pytest
from pr_agent.algo.utils import load_yaml
class TestLoadYaml:
# Tests that load_yaml loads a valid YAML string
def test_load_valid_yaml(self):
yaml_str = 'name: John Smith\nage: 35'
expected_output = {'name': 'John Smith', 'age': 35}
assert load_yaml(yaml_str) == expected_output
def test_load_complicated_yaml(self):
yaml_str = \
'''\
PR Analysis:
Main theme: Enhancing the `/describe` command prompt by adding title and description
Type of PR: Enhancement
Relevant tests added: No
Focused PR: Yes, the PR is focused on enhancing the `/describe` command prompt.
PR Feedback:
General suggestions: The PR seems to be well-structured and focused on a specific enhancement. However, it would be beneficial to add tests to ensure the new feature works as expected.
Code feedback:
- relevant file: pr_agent/settings/pr_description_prompts.toml
suggestion: Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium]
relevant line: 'user="""PR Info:'
Security concerns: No'''
expected_output = {'PR Analysis': {'Main theme': 'Enhancing the `/describe` command prompt by adding title and description', 'Type of PR': 'Enhancement', 'Relevant tests added': False, 'Focused PR': 'Yes, the PR is focused on enhancing the `/describe` command prompt.'}, 'PR Feedback': {'General suggestions': 'The PR seems to be well-structured and focused on a specific enhancement. However, it would be beneficial to add tests to ensure the new feature works as expected.', 'Code feedback': [{'relevant file': 'pr_agent/settings/pr_description_prompts.toml', 'suggestion': "Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium]", 'relevant line': 'user="""PR Info:'}], 'Security concerns': False}}
assert load_yaml(yaml_str) == expected_output