Compare commits

..

51 Commits

Author SHA1 Message Date
3b27c834a4 Merge remote-tracking branch 'origin/main' into ok/update_readme 2023-07-19 11:14:44 +03:00
5bc2ef1eff Merge pull request #92 from YuviGold/deploy-on-lambda
Deployment on AWS Lambda
2023-07-19 11:12:29 +03:00
2f558006bf Update INSTALL.md, add notes about injecting secrets 2023-07-19 11:09:35 +03:00
370520df51 Update docker/Dockerfile.lambda
have a fixed mangum version

Co-authored-by: Ori Kotek <orikotek@gmail.com>
2023-07-19 11:05:24 +03:00
2e832b8fb4 Merge pull request #86 from Codium-ai/GadiZimerman-patch-1
Update README.md
2023-07-19 10:51:31 +03:00
a47fa342cb Merge pull request #88 from zmeir/zmeir-cli_args
CLI Arguments Refactoring
2023-07-19 08:15:19 +03:00
bdf7eff7cd Merge pull request #87 from Codium-ai/tr/bug_fix
Add Insights from User's Answers and Fix User Answers Fetching
2023-07-18 18:20:15 +03:00
dc67e6a66e Support deploying pr-agent on AWS Lambda 2023-07-18 17:46:42 +03:00
77f243b7ab Allow passing CLI args (helps with debugging) 2023-07-18 16:39:46 +03:00
c507785475 bugfix 2023-07-18 16:32:51 +03:00
5c5015b267 Update README.md 2023-07-18 14:45:15 +03:00
3efe08d619 Merge pull request #85 from Codium-ai/hl/always_filer_bad_extensions
Filter out bad files before getting their head and original source code and diff
2023-07-18 13:50:25 +03:00
2e36fce4eb Merge pull request #83 from Codium-ai/hl/gitlab_description
Support describe for Gitlab
2023-07-18 13:47:32 +03:00
d6d4427545 Merge pull request #84 from Codium-ai/GadiZimerman-patch-1
Update README.md
2023-07-18 13:37:43 +03:00
5d45632247 Performance improvement: Filter out bad files before getting their head and original source code and diff 2023-07-18 13:33:32 +03:00
90c045e3d0 Update README.md
changing image
2023-07-18 13:26:19 +03:00
7f0a96d8f7 readme 2023-07-18 13:17:30 +03:00
8fb9affef3 add try catch 2023-07-18 13:14:01 +03:00
6c42a471e1 Merge pull request #76 from zmeir/zmeir-publish_inline_comments_single_api_call
Optimization of Inline Comments Publishing
2023-07-18 13:05:11 +03:00
f2b74b6970 support gitlab describe function 2023-07-18 13:03:36 +03:00
ffd11aeffc Merge pull request #81 from Codium-ai/GadiZimerman-patch-1
Update README.md
2023-07-18 12:55:26 +03:00
05e4e09dfc Lint 2023-07-18 12:27:28 +03:00
13092118dc Move the new git provider function to the abstract interface 2023-07-18 12:26:49 +03:00
7d108992fc Merge remote-tracking branch 'origin/main' into zmeir-publish_inline_comments_single_api_call 2023-07-18 11:53:41 +03:00
e5a8ed205e Merge pull request #82 from Codium-ai/ok/lint
Linting and Code Cleanup
2023-07-18 11:40:43 +03:00
90f97b0226 Lint fixes 2023-07-18 11:34:57 +03:00
9e0f5f0ccc Merge pull request #78 from Codium-ai/tr/agent_logic
Enhancement of PR Agent with User Interaction
2023-07-18 10:37:08 +03:00
87ea0176b9 Update README.md 2023-07-18 10:36:05 +03:00
62f08f4ec4 removed an unneeded file 2023-07-18 10:35:05 +03:00
fe0058f25f Merge branch 'tr/agent_logic' of github.com:Codium-ai/pr-agent into tr/agent_logic 2023-07-18 10:34:40 +03:00
6d2673f39d Merge remote-tracking branch 'origin/tr/agent_logic' into tr/agent_logic 2023-07-18 10:32:43 +03:00
b3a1d456b2 if settings.pr_reviewer.num_code_suggestions 2023-07-18 10:32:36 +03:00
f77a5f6929 Call PRAgent from github_action_runner.py 2023-07-18 10:31:24 +03:00
fdeae9c209 Update pr_agent/agent/pr_agent.py 2023-07-18 10:20:52 +03:00
a994ec1427 Call PRAgent from github_action_runner.py 2023-07-18 10:19:32 +03:00
e5259e2f5c Small refactor 2023-07-18 10:17:09 +03:00
978348240b Update README.md 2023-07-18 09:59:47 +03:00
4d92e7d9c2 Update README.md
consider changing section headers to reflect commands format
2023-07-18 09:56:40 +03:00
6f1b418b25 Merge pull request #79 from patryk-kowalski-ds/deepsense.ai/gitlab-provider-file-creation-handling
Fixes 404 error on gitlab file provider happening in case a MR introduced a new file.
2023-07-18 08:27:59 +03:00
51e08c3c2b reflect and review + protections 2023-07-18 08:22:25 +03:00
4c29ff2db1 Merge remote-tracking branch 'origin/tr/agent_logic' into tr/agent_logic
# Conflicts:
#	pr_agent/tools/pr_description.py
2023-07-18 08:06:47 +03:00
5fbaa4366f publish_output instead publish_review 2023-07-18 08:05:42 +03:00
aee08ebbfe Merge branch 'main' into tr/agent_logic 2023-07-18 08:04:47 +03:00
6ad8df6be7 Merge pull request #80 from Codium-ai/ok/remove_pics
Remove most pics from repo
2023-07-17 23:51:24 +03:00
539edcad3c works 2023-07-17 16:53:38 +03:00
ea27c63f13 Insights from user's answers 2023-07-17 15:59:57 +03:00
c866288b0a Merge remote-tracking branch 'origin/main' into tr/agent_logic 2023-07-17 15:59:37 +03:00
8ae3c60670 In case of new file creation by the MR there is a 404 error on file retrieval by gitlab provider.
It was handled by catching the error and replacing the file string with an empty string.
Type checking was added before byte decoding - necessary in case of the empty string.
2023-07-17 14:53:23 +02:00
f8f415eb75 stable 2023-07-17 15:49:29 +03:00
24583b05f7 Publish GitHub review comments with single API call 2023-07-17 10:41:02 +03:00
fa90b242e3 pr_information_from_user_prompts 2023-07-17 08:09:56 +03:00
27 changed files with 320 additions and 118 deletions

View File

@ -149,16 +149,35 @@ git clone https://github.com/Codium-ai/pr-agent.git
```
5. Copy the secrets template file and fill in the following:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
- Your OpenAI key.
- Set deployment_type to 'app'
- Copy your app's private key to the private_key field.
- Copy your app's ID to the app_id field.
- Copy your app's webhook secret to the webhook_secret field.
- Set deployment_type to 'app' in [configuration.toml](./pr_agent/settings/configuration.toml)
> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development.
> If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file.
> In most production environments, you would inject the secrets file as environment variables or as mounted volumes.
> For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following,
> assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`:
```
volumes:
- name: settings-volume
secret:
secretName: pr-agent-settings
// ...
containers:
// ...
volumeMounts:
- mountPath: /app/pr_agent/settings_prod
name: settings-volume
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
> Another option is to set the secrets as environment variables in your deployment environment, for example `OPENAI.KEY` and `GITHUB.USER_TOKEN`.
6. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
@ -169,6 +188,7 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
7. Host the app using a server, serverless function, or container environment. Alternatively, for development and
debugging, you may use tools like smee.io to forward webhooks to your local machine.
You can check [Deploy as a Lambda Function](#deploy-as-a-lambda-function)
8. Go back to your app's settings, and set the following:
@ -178,3 +198,20 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
---
#### Deploy as a Lambda Function
1. Follow steps 1-5 of [Method 5](#method-5-run-as-a-github-app).
2. Build a docker image that can be used as a lambda function
```shell
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
```
3. Push image to ECR
```shell
docker tag codiumai/pr-agent:serverless <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
docker push <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
```
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
5. Configure the lambda function to have a Function URL.
6. Go back to steps 8-9 of [Method 5](#method-5-run-as-a-github-app) with the function url as your Webhook URL.
The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks`

View File

@ -24,25 +24,25 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review PRs
<h3>Example results:</h2>
</div>
<h4>Describe:</h4>
<h4>/describe:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/describe.gif" width="800">
</p>
</div>
<h4>Review:</h4>
<h4>/review:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/review.gif" width="800">
</p>
</div>
<h4>Ask:</h4>
<h4>/ask:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/ask.gif" width="800">
</p>
</div>
<h4>Improve:</h4>
<h4>/improve:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/improve.gif" width="800">
@ -76,8 +76,9 @@ To set up your own PR-Agent, see the [Quickstart](#Quickstart) section
| 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: | |
| | Auto-Description | :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: | | |
| | | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Tagging bot | :white_check_mark: | :white_check_mark: | |
@ -92,6 +93,7 @@ Examples for invoking the different tools via the [CLI](#quickstart):
- **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
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
@ -120,16 +122,17 @@ Here are several ways to install and run PR-Agent:
## Usage and Tools
**PR-Agent** provides four types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"` and `"PR Code Sueggestions"`.
**PR-Agent** provides five types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"`, `"PR Code Sueggestions"` and `"PR Reflect and Review"`.
- The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback.
- The "PR Q&A" tool answers free-text questions about the PR.
- The "PR Description" tool automatically sets the PR Title and body.
- The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed.
- The "PR Reflect and Review" tool first initiates a dialog with the user and asks them to reflect on the PR, and then provides a review.
## How it works
![PR-Agent Tools](https://codium.ai/images/pr_agent_overview.png)
![PR-Agent Tools](https://www.codium.ai/wp-content/uploads/2023/07/codiumai-diagram-v4.jpg)
Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt
@ -138,11 +141,11 @@ Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more detai
- [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output)
- [x] Support other Git providers, such as Gitlab and Bitbucket.
- [ ] Develop additional logics for handling large PRs, and compressing git patches
- [ ] Dedicated tools and sub-tools for specific programming languages (Python, Javascript, Java, C++, etc)
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
- [ ] Adding more tools. Possible directions:
- [x] PR description
- [x] Inline code suggestions
- [x] Reflect and review
- [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues)
- [ ] Documentation (is the PR properly documented)

12
docker/Dockerfile.lambda Normal file
View File

@ -0,0 +1,12 @@
FROM public.ecr.aws/lambda/python:3.10
RUN yum update -y && \
yum install -y gcc python3-devel && \
yum clean all
ADD requirements.txt .
RUN pip install -r requirements.txt && rm requirements.txt
RUN pip install mangum==16.0.0
COPY pr_agent/ ${LAMBDA_TASK_ROOT}/pr_agent/
CMD ["pr_agent.servers.serverless.serverless"]

BIN
pics/.DS_Store vendored

Binary file not shown.

View File

@ -1,7 +1,9 @@
import re
from pr_agent.config_loader import settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
@ -11,8 +13,13 @@ class PRAgent:
pass
async def handle_request(self, pr_url, request) -> bool:
if any(cmd in request for cmd in ["/review", "/review_pr"]):
await PRReviewer(pr_url).review()
if any(cmd in request for cmd in ["/answer"]):
await PRReviewer(pr_url, is_answer=True).review()
elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]):
if settings.pr_reviewer.ask_and_reflect or "/reflect_and_review" in request:
await PRInformationFromUser(pr_url).generate_questions()
else:
await PRReviewer(pr_url).review()
elif any(cmd in request for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe()
elif any(cmd in request for cmd in ["/improve", "/improve_code"]):

View File

@ -158,7 +158,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines:
patch_with_lines_str += '--old hunk--\n'
for i, line_old in enumerate(old_content_lines):
for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n"
new_content_lines = []
old_content_lines = []
@ -179,7 +179,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines:
patch_with_lines_str += '\n--old hunk--\n'
for i, line_old in enumerate(old_content_lines):
for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n"
return patch_with_lines_str.strip()

View File

@ -64,7 +64,11 @@ bad_extensions = [
def filter_bad_extensions(files):
return [f for f in files if f.filename.split('.')[-1] not in bad_extensions]
return [f for f in files if is_valid_file(f.filename)]
def is_valid_file(filename):
return filename.split('.')[-1] not in bad_extensions
def sort_files_by_main_languages(languages: Dict, files: list):

View File

@ -4,8 +4,7 @@ import difflib
import logging
from typing import Any, Tuple, Union
from pr_agent.algo.git_patch_processing import extend_patch, handle_patch_deletions, \
convert_to_hunks_with_lines_numbers
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
from pr_agent.algo.language_handler import sort_files_by_main_languages
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import settings
@ -30,10 +29,10 @@ def get_pr_diff(git_provider: Union[GithubProvider, Any], token_handler: TokenHa
global PATCH_EXTRA_LINES
PATCH_EXTRA_LINES = 0
git_provider.pr.diff_files = list(git_provider.get_diff_files())
diff_files = list(git_provider.get_diff_files())
# get pr languages
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), git_provider.pr.diff_files)
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
# generate a standard diff string, with patch extension
patches_extended, total_tokens = pr_generate_extended_diff(pr_languages, token_handler,

View File

@ -17,6 +17,7 @@ def convert_to_markdown(output_data: dict) -> str:
"Focused PR": "",
"Security concerns": "🔒",
"General PR suggestions": "💡",
"Insights from user's answers": "📝",
"Code suggestions": "🤖"
}
@ -89,8 +90,8 @@ def try_fix_json(review, max_iter=10, code_suggestions=False):
data = {}
return data
def fix_json_escape_char(json_message=None):
result = None
try:
result = json.loads(json_message)
except Exception as e:
@ -100,5 +101,5 @@ def fix_json_escape_char(json_message=None):
json_message = list(json_message)
json_message[idx_to_replace] = ' '
new_message = ''.join(json_message)
return fix_JSON(json_message=new_message)
return result
return fix_json_escape_char(json_message=new_message)
return result

View File

@ -10,7 +10,7 @@ from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
def run():
def run(args=None):
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage="""\
Usage: cli.py --pr-url <URL on supported git hosting service> <command> [<args>].
For example:
@ -18,21 +18,24 @@ For example:
- 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.
ask / ask_question [question] - Ask a question about the PR.
describe / describe_pr - Modify the PR title and description based on the PR's contents.
improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
reflect - Ask the PR author questions about the PR.
""")
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=['review', 'review_pr',
'ask', 'ask_question',
'describe', 'describe_pr',
'improve', 'improve_code',
'user_questions'], default='review')
'reflect', 'review_after_reflect'],
default='review')
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
args = parser.parse_args()
args = parser.parse_args(args)
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
command = args.command.lower()
if command in ['ask', 'ask_question']:
@ -56,10 +59,14 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ
print(f"Reviewing PR: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True)
asyncio.run(reviewer.review())
elif command in ['user_questions']:
elif command in ['reflect']:
print(f"Asking the PR author questions: {args.pr_url}")
reviewer = PRInformationFromUser(args.pr_url)
asyncio.run(reviewer.generate_questions())
elif command in ['review_after_reflect']:
print(f"Processing author's answers and sending review: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True)
asyncio.run(reviewer.review())
else:
print(f"Unknown command: {command}")
parser.print_help()

View File

@ -1,7 +1,7 @@
from pr_agent.config_loader import settings
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
from pr_agent.git_providers.github_provider import GithubProvider
from pr_agent.git_providers.gitlab_provider import GitLabProvider
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
_GIT_PROVIDERS = {
'github': GithubProvider,

View File

@ -1,5 +1,4 @@
import logging
from datetime import datetime
from typing import Optional, Tuple
from urllib.parse import urlparse
@ -10,6 +9,7 @@ from pr_agent.config_loader import settings
from .git_provider import FilePatchInfo
class BitbucketProvider:
def __init__(self, pr_url: Optional[str] = None):
s = requests.Session()
@ -25,6 +25,11 @@ class BitbucketProvider:
if pr_url:
self.set_pr(pr_url)
def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
return False
return True
def set_pr(self, pr_url: str):
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
@ -40,7 +45,8 @@ class BitbucketProvider:
for index, diff in enumerate(diffs):
original_file_content_str = self._get_pr_file_content(diff.old.get_data('links'))
new_file_content_str = self._get_pr_file_content(diff.new.get_data('links'))
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, diff_split[index], diff.new.path))
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str,
diff_split[index], diff.new.path))
return diff_files
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
@ -58,6 +64,12 @@ class BitbucketProvider:
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError("Bitbucket provider does not support creating inline comments yet")
def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError("Bitbucket provider does not support publishing inline comments yet")
def get_title(self):
return self.pr.title
@ -74,6 +86,9 @@ class BitbucketProvider:
def get_user_id(self):
return 0
def get_issue_comments(self):
raise NotImplementedError("Bitbucket provider does not support issue comments yet")
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)

View File

@ -3,12 +3,15 @@ from dataclasses import dataclass
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
from enum import Enum
class EDIT_TYPE(Enum):
ADDED = 1
DELETED = 2
MODIFIED = 3
RENAMED = 4
@dataclass
class FilePatchInfo:
base_file: str
@ -21,6 +24,10 @@ class FilePatchInfo:
class GitProvider(ABC):
@abstractmethod
def is_supported(self, capability: str) -> bool:
pass
@abstractmethod
def get_diff_files(self) -> list[FilePatchInfo]:
pass
@ -37,6 +44,14 @@ class GitProvider(ABC):
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@abstractmethod
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@abstractmethod
def publish_inline_comments(self, comments: list[dict]):
pass
@abstractmethod
def publish_code_suggestion(self, body: str, relevant_file: str,
relevant_lines_start: int, relevant_lines_end: int):
@ -62,6 +77,10 @@ class GitProvider(ABC):
def get_pr_description(self):
pass
@abstractmethod
def get_issue_comments(self):
pass
def get_main_pr_language(languages, files) -> str:
"""

View File

@ -3,11 +3,12 @@ from datetime import datetime
from typing import Optional, Tuple
from urllib.parse import urlparse
from github import AppAuthentication, Github
from github import AppAuthentication, Github, Auth
from pr_agent.config_loader import settings
from .git_provider import FilePatchInfo, GitProvider
from ..algo.language_handler import is_valid_file
class GithubProvider(GitProvider):
@ -23,6 +24,9 @@ class GithubProvider(GitProvider):
self.set_pr(pr_url)
self.last_commit_id = list(self.pr.get_commits())[-1]
def is_supported(self, capability: str) -> bool:
return True
def set_pr(self, pr_url: str):
self.repo, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
@ -34,9 +38,10 @@ class GithubProvider(GitProvider):
files = self.pr.get_files()
diff_files = []
for file in files:
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha)
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, file.patch, file.filename))
if is_valid_file(file.filename):
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha)
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, file.patch, file.filename))
self.diff_files = diff_files
return diff_files
@ -54,6 +59,9 @@ class GithubProvider(GitProvider):
self.pr.comments_list.append(response)
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)])
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
self.diff_files = self.diff_files if self.diff_files else self.get_diff_files()
position = -1
for file in self.diff_files:
@ -64,7 +72,7 @@ class GithubProvider(GitProvider):
if relevant_line_in_file in line:
position = i
break
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:] in line:
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:].lstrip() in line:
# The model often adds a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
position = i
@ -72,9 +80,16 @@ class GithubProvider(GitProvider):
if position == -1:
if settings.config.verbosity_level >= 2:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
subject_type = "FILE"
else:
path = relevant_file.strip()
self.pr.create_review_comment(body=body, commit_id=self.last_commit_id, path=path, position=position)
subject_type = "LINE"
path = relevant_file.strip()
# placeholder for future API support (already supported in single inline comment)
# return dict(body=body, path=path, position=position, subject_type=subject_type)
return dict(body=body, path=path, position=position) if subject_type == "LINE" else {}
def publish_inline_comments(self, comments: list[dict]):
self.pr.create_review(commit=self.last_commit_id, comments=comments)
def publish_code_suggestion(self, body: str,
relevant_file: str,
@ -161,6 +176,9 @@ class GithubProvider(GitProvider):
notifications = self.github_client.get_user().get_notifications(since=since)
return notifications
def get_issue_comments(self):
return self.pr.get_issue_comments()
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)
@ -212,7 +230,7 @@ class GithubProvider(GitProvider):
raise ValueError(
"GitHub token is required when using user deployment. See: "
"https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e
return Github(token)
return Github(auth=Auth.Token(token))
def _get_repo(self):
return self.github_client.get_repo(self.repo)

View File

@ -4,10 +4,12 @@ from typing import Optional, Tuple
from urllib.parse import urlparse
import gitlab
from gitlab import GitlabGetError
from pr_agent.config_loader import settings
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..algo.language_handler import is_valid_file
class GitLabProvider(GitProvider):
@ -31,6 +33,11 @@ class GitLabProvider(GitProvider):
self.RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
return False
return True
@property
def pr(self):
'''The GitLab terminology is merge request (MR) instead of pull request (PR)'''
@ -42,31 +49,39 @@ class GitLabProvider(GitProvider):
self.last_diff = self.mr.diffs.list()[-1]
def _get_pr_file_content(self, file_path: str, branch: str) -> str:
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
try:
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
except GitlabGetError:
# In case of file creation the method returns GitlabGetError (404 file not found).
# In this case we return an empty string for the diff.
return ''
def get_diff_files(self) -> list[FilePatchInfo]:
diffs = self.mr.changes()['changes']
diff_files = []
for diff in diffs:
original_file_content_str = self._get_pr_file_content(diff['old_path'], self.mr.target_branch)
new_file_content_str = self._get_pr_file_content(diff['new_path'], self.mr.source_branch)
edit_type = EDIT_TYPE.MODIFIED
if diff['new_file']:
edit_type = EDIT_TYPE.ADDED
elif diff['deleted_file']:
edit_type = EDIT_TYPE.DELETED
elif diff['renamed_file']:
edit_type = EDIT_TYPE.RENAMED
try:
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
except UnicodeDecodeError:
logging.warning(
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
diff_files.append(
FilePatchInfo(original_file_content_str, new_file_content_str, diff['diff'], diff['new_path'],
edit_type=edit_type,
old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path']))
if is_valid_file(diff['new_path']):
original_file_content_str = self._get_pr_file_content(diff['old_path'], self.mr.target_branch)
new_file_content_str = self._get_pr_file_content(diff['new_path'], self.mr.source_branch)
edit_type = EDIT_TYPE.MODIFIED
if diff['new_file']:
edit_type = EDIT_TYPE.ADDED
elif diff['deleted_file']:
edit_type = EDIT_TYPE.DELETED
elif diff['renamed_file']:
edit_type = EDIT_TYPE.RENAMED
try:
if isinstance(original_file_content_str, bytes):
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
if isinstance(new_file_content_str, bytes):
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
except UnicodeDecodeError:
logging.warning(
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
diff_files.append(
FilePatchInfo(original_file_content_str, new_file_content_str, diff['diff'], diff['new_path'],
edit_type=edit_type,
old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path']))
self.diff_files = diff_files
return diff_files
@ -74,8 +89,12 @@ class GitLabProvider(GitProvider):
return [change['new_path'] for change in self.mr.changes()['changes']]
def publish_description(self, pr_title: str, pr_body: str):
logging.exception("Not implemented yet")
pass
try:
self.mr.title = pr_title
self.mr.description = pr_body
self.mr.save()
except Exception as e:
logging.exception(f"Could not update merge request {self.id_mr} description: {e}")
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
comment = self.mr.notes.create({'body': mr_comment})
@ -89,6 +108,12 @@ class GitLabProvider(GitProvider):
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no)
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError("Gitlab provider does not support creating inline comments yet")
def create_inline_comment(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):
if not found:
@ -125,7 +150,8 @@ class GitLabProvider(GitProvider):
lines = target_file.head_file.splitlines()
relevant_line_in_file = lines[relevant_lines_start - 1]
edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file, relevant_line_in_file)
edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file,
relevant_line_in_file)
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no)
@ -147,7 +173,7 @@ class GitLabProvider(GitProvider):
target_file = file
patch = file.patch
patch_lines = patch.splitlines()
for i, line in enumerate(patch_lines):
for line in patch_lines:
if line.startswith('@@'):
match = self.RE_HUNK_HEADER.match(line)
if not match:
@ -167,7 +193,7 @@ class GitLabProvider(GitProvider):
found = True
edit_type = self.get_edit_type(line)
break
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:] in line:
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:].lstrip() in line:
# The model often adds a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
found = True
@ -203,6 +229,9 @@ class GitLabProvider(GitProvider):
def get_pr_description(self):
return self.mr.description
def get_issue_comments(self):
raise NotImplementedError("GitLab provider does not support issue comments yet")
def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]:
parsed_url = urlparse(merge_request_url)

View File

@ -1,12 +1,9 @@
import asyncio
import json
import os
import re
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
@ -53,20 +50,7 @@ async def run_action():
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
if pr_url:
body = comment_body.strip().lower()
if any(cmd in body for cmd in ["/review", "/review_pr"]):
await PRReviewer(pr_url).review()
elif any(cmd in body for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe()
elif any(cmd in body for cmd in ["/improve", "/improve_code"]):
await PRCodeSuggestions(pr_url).suggest()
elif any(cmd in body for cmd in ["/ask", "/ask_question"]):
pattern = r'(/ask|/ask_question)\s*(.*)'
matches = re.findall(pattern, comment_body, re.IGNORECASE)
if matches:
question = matches[0][1]
await PRQuestions(pr_url, question).answer()
else:
print(f"Unknown command: {body}")
await PRAgent().handle_request(pr_url, body)
if __name__ == '__main__':

View File

@ -1,6 +1,5 @@
import asyncio
import logging
import re
import sys
from datetime import datetime, timezone
@ -10,10 +9,6 @@ from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider
from pr_agent.servers.help import bot_help_text
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
NOTIFICATION_URL = "https://api.github.com/notifications"
@ -103,5 +98,6 @@ async def polling_loop():
except Exception as e:
logging.error(f"Exception during processing of a notification: {e}")
if __name__ == '__main__':
asyncio.run(polling_loop())

View File

@ -0,0 +1,18 @@
import logging
from fastapi import FastAPI
from mangum import Mangum
from pr_agent.servers.github_app import router
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
app = FastAPI()
app.include_router(router)
handler = Mangum(app, lifespan="off")
def serverless(event, context):
return handler(event, context)

View File

@ -10,6 +10,7 @@ require_tests_review=true
require_security_review=true
num_code_suggestions=3
inline_code_comments = true
ask_and_reflect=false
[pr_description]
publish_description_as_comment=false

View File

@ -1,16 +1,17 @@
[pr_information_from_user_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Given the PR Info and the PR Git Diff, generate 4 questions about the PR for the PR author.
Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author.
The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR.
Prefer yes\\no or multiple choice questions. If you have to ask open-ended questions, make sure they are not too difficult, and can be answered in a sentence or two.
You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two.
Example output:
'
Questions to better understand the PR:
1. ...
2. ...
1) ...
2) ...
...
'
"""
user="""PR Info:

View File

@ -26,6 +26,12 @@ You must use the following JSON schema to format your answer:
"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"
},
{%- endif %}
{%- if require_focused %}
"Focused PR": {
"type": "string",
@ -115,6 +121,16 @@ Description: '{{description}}'
Main language: {{language}}
{%- endif %}
{%- if question_str %}
######
Here are questions to better understand the PR. Use the answers to provide better feedback.
{{question_str|trim}}
User answers:
{{answer_str|trim}}
######
{%- endif %}
The PR Git Diff:
```

View File

@ -8,9 +8,9 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff
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 try_fix_json
from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider, BitbucketProvider
from pr_agent.git_providers import BitbucketProvider, get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
@ -62,7 +62,6 @@ class PRCodeSuggestions:
logging.info('Pushing inline code comments...')
self.push_inline_code_suggestions(data)
async def _get_prediction(self):
variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff
@ -98,12 +97,12 @@ class PRCodeSuggestions:
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
relevant_lines_end = int(relevant_lines_str.split('-')[-1])
content = d['suggestion content']
existing_code_snippet = d['existing code']
new_code_snippet = d['improved code']
if new_code_snippet:
try: # dedent code snippet
self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files else self.git_provider.get_diff_files()
self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files \
else self.git_provider.get_diff_files()
original_initial_line = None
for file in self.diff_files:
if file.filename.strip() == relevant_file:
@ -121,7 +120,7 @@ class PRCodeSuggestions:
logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```"
success = self.git_provider.publish_code_suggestion(body=body,
self.git_provider.publish_code_suggestion(body=body,
relevant_file=relevant_file,
relevant_lines_start=relevant_lines_start,
relevant_lines_end=relevant_lines_end)

View File

@ -7,7 +7,6 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import convert_to_markdown
from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language

View File

@ -35,7 +35,7 @@ class PRInformationFromUser:
async def generate_questions(self):
logging.info('Generating question to the user...')
if settings.config.publish_output:
self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
logging.info('Getting AI prediction...')
@ -66,6 +66,6 @@ class PRInformationFromUser:
model_output = self.prediction.strip()
if settings.config.verbosity_level >= 2:
logging.info(f"answer_str:\n{model_output}")
answer_str = f"{model_output}\n\n Please respond to the question above in the following format:\n\n" + \
f"/answer <question_id> <answer>\n\n" + f"Example:\n'\n/answer\n1. Yes, because ...\n2. No, because ...\n'"
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
"\n>/answer\n>1) ...\n>2) ...\n>...\n"
return answer_str

View File

@ -11,16 +11,20 @@ from pr_agent.algo.utils import convert_to_markdown, try_fix_json
from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.servers.help import bot_help_text, actions_help_text
from pr_agent.servers.help import actions_help_text, bot_help_text
class PRReviewer:
def __init__(self, pr_url: str, cli_mode=False):
def __init__(self, pr_url: str, cli_mode=False, is_answer: bool = False):
self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self.is_answer = is_answer
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now")
answer_str, question_str = self._get_user_answers()
self.ai_handler = AiHandler()
self.patches_diff = None
self.prediction = None
@ -35,6 +39,9 @@ class PRReviewer:
"require_security": settings.pr_reviewer.require_security_review,
"require_focused": settings.pr_reviewer.require_focused_review,
'num_code_suggestions': settings.pr_reviewer.num_code_suggestions,
#
'question_str': question_str,
'answer_str': answer_str,
}
self.token_handler = TokenHandler(self.git_provider.pr,
self.vars,
@ -44,7 +51,7 @@ class PRReviewer:
async def review(self):
logging.info('Reviewing PR...')
if settings.config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
logging.info('Getting AI prediction...')
@ -89,10 +96,16 @@ class PRReviewer:
del data['PR Feedback']['Security concerns']
data['PR Analysis']['Security concerns'] = val
if settings.config.git_provider == 'github' and \
if settings.config.git_provider != 'bitbucket' and \
settings.pr_reviewer.inline_code_comments and \
'Code suggestions' in data['PR Feedback']:
del data['PR Feedback']['Code suggestions']
# keeping only code suggestions that can't be submitted as inline comments
data['PR Feedback']['Code suggestions'] = [
d for d in data['PR Feedback']['Code suggestions']
if any(key not in d for key in ('relevant file', 'relevant line in file', 'suggestion content'))
]
if not data['PR Feedback']['Code suggestions']:
del data['PR Feedback']['Code suggestions']
markdown_text = convert_to_markdown(data)
user = self.git_provider.get_user_id()
@ -118,9 +131,34 @@ class PRReviewer:
except json.decoder.JSONDecodeError:
data = try_fix_json(review)
comments = []
for d in data['PR Feedback']['Code suggestions']:
relevant_file = d['relevant file'].strip()
relevant_line_in_file = d['relevant line in file'].strip()
content = d['suggestion content']
relevant_file = d.get('relevant file', '').strip()
relevant_line_in_file = d.get('relevant line in file', '').strip()
content = d.get('suggestion content', '')
if not relevant_file or not relevant_line_in_file or not content:
logging.info("Skipping inline comment with missing file/line/content")
continue
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
if self.git_provider.is_supported("create_inline_comment"):
comment = self.git_provider.create_inline_comment(content, relevant_file, relevant_line_in_file)
if comment:
comments.append(comment)
else:
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
if comments:
self.git_provider.publish_inline_comments(comments)
def _get_user_answers(self):
answer_str = question_str = ""
if self.is_answer:
discussion_messages = self.git_provider.get_issue_comments()
for message in discussion_messages.reversed:
if "Questions to better understand the PR:" in message.body:
question_str = message.body
elif '/answer' in message.body:
answer_str = message.body
if answer_str and question_str:
break
return question_str, answer_str

View File

@ -1,6 +1,6 @@
dynaconf==3.1.12
fastapi==0.99.0
PyGithub==1.58.2
PyGithub==1.59.*
retry==0.9.2
openai==0.27.8
Jinja2==3.1.2

View File

@ -1,13 +1,12 @@
# Generated by CodiumAI
from pr_agent.algo.utils import try_fix_json
import pytest
class TestTryFixJson:
# Tests that JSON with complete 'Code suggestions' section returns expected output
def test_incomplete_code_suggestions(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...'
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
expected_output = {
'PR Analysis': {
'Main theme': 'xxx',
@ -26,7 +25,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_new_line(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...'
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
expected_output = {
'PR Analysis': {
'Main theme': 'xxx',
@ -45,7 +44,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_many_close_brackets(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..'
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..' # noqa: E501
expected_output = {
'PR Analysis': {
'Main theme': 'xxx',
@ -64,7 +63,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_relevant_file(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p'
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p' # noqa: E501
expected_output = {
'PR Analysis': {
'Main theme': 'xxx',