Compare commits

...

181 Commits

Author SHA1 Message Date
7005a0466a Update 2023-12-12 12:34:28 +02:00
648dd3299f Merge pull request #521 from Codium-ai/tr/bitbucket_app
feat: Enable PR description publishing as comment in bitbucket_app.py
2023-12-12 00:27:48 -08:00
77a6fafdfc feat: Update Usage.md with limitations of bitbucket platform in auto_describe tool usage 2023-12-12 10:26:09 +02:00
ea7511e3c8 feat: Update Usage.md with limitations of bitbucket platform in auto_describe tool usage 2023-12-12 10:23:37 +02:00
512c92fe51 feat: Enable PR description publishing as comment in bitbucket_app.py 2023-12-12 10:19:17 +02:00
1853b4ef47 Merge pull request #520 from Codium-ai/tr/bitbucket_app
Refactor auto tool execution order and enhance logging and documentation
2023-12-12 00:02:02 -08:00
2f10b4f3c5 feat: Refactor auto tool execution order in bitbucket_app.py, add logging in pr_description.py, and update tool configuration instructions in Usage.md 2023-12-12 09:59:26 +02:00
73a20076eb Merge pull request #519 from Codium-ai/tr/bitbucket_app
Enhancement: Automatic Tool Configuration for Bitbucket App
2023-12-11 23:26:30 -08:00
afb633811f remove bad default 2023-12-12 09:18:51 +02:00
81da328ae3 feat: Add automatic tool configuration for Bitbucket app in bitbucket_app.py and configuration.toml, update Usage.md 2023-12-12 08:06:20 +02:00
729f5e9c8e Merge pull request #518 from Codium-ai/hl/github_native_labels
Refactoring Label Handling Across Git Providers
2023-12-11 16:50:19 +02:00
fdc776887d Refactor labels 2023-12-11 16:47:38 +02:00
cb64f92cce Merge pull request #511 from Codium-ai/tr/local_settings_on_push
Enhancement: Apply Repository Settings on Every 'Synchronize' Event
2023-12-11 06:27:29 -08:00
f3ad0e1d2a Merge pull request #517 from Codium-ai/tr/main_tmp
Improve PR description formatting and handling in pr_description.py
2023-12-11 06:15:46 -08:00
480e2ee678 feat: Improve PR description formatting in pr_description.py 2023-12-11 15:55:04 +02:00
9b97073174 s 2023-12-11 12:00:44 +02:00
4271bb7e52 Merge pull request #516 from Codium-ai/coditamar-readme-clarifications
Refine README.md for clarity and precision
2023-12-11 01:53:46 -08:00
e9bf8574a8 Update README.md 2023-12-11 11:52:36 +02:00
2ce4af16cb Update README.md
fix grammar according to PR-Agent suggestions
2023-12-11 11:10:27 +02:00
2c1dfe7f3f Update README.md 2023-12-11 10:58:30 +02:00
f7a6348401 Merge pull request #515 from Codium-ai/tr/review_graphics
Enhancement of Code Feedback Formatting in utils.py
2023-12-10 22:37:18 -08:00
02c0c89b13 feat: Add exception handling for discussion creation in gitlab_provider.py 2023-12-11 08:29:09 +02:00
b8cc110cbe s 2023-12-10 19:51:08 +02:00
2b1e841ef1 s 2023-12-10 19:45:54 +02:00
a247fc3263 s 2023-12-10 17:46:49 +02:00
654938f27c feat: Enhance code feedback formatting in utils.py 2023-12-10 17:30:27 +02:00
a7a0de764c Merge pull request #512 from Codium-ai/trlabeling_files_extended
Refactoring and Enhancement of PR Description Formatting in 'pr_description.py'
2023-12-07 05:38:37 -08:00
1b22e59b4b feat: Update RELEASE_NOTES.md with version 0.11 details 2023-12-07 15:35:48 +02:00
f908d02ab4 readme 2023-12-07 15:26:36 +02:00
7d2a35e32c final commit 2023-12-07 10:27:19 +02:00
e351428848 s 2023-12-07 10:24:36 +02:00
4cd6649a44 feat: Enhance PR description formatting in pr_description.py
Improve the table structure for relevant files in PR description by adjusting the header and filename display. Add padding for filename and change summary, and move diff_plus_minus to a separate column. Refactor _insert_br_after_x_chars function to accept a variable length parameter.
2023-12-07 10:14:18 +02:00
e62acef6d2 s1 2023-12-07 09:50:36 +02:00
a043eb939b feat: Apply repo settings on push trigger in github_app.py 2023-12-07 08:42:18 +02:00
73eafa2c3d Merge pull request #509 from Codium-ai/trlabeling_files_extended
Enhancement of PR Description and Labeling Features
2023-12-06 07:05:22 -08:00
a61e492fe1 feat: Refactor PR files processing into separate function in pr_description.py 2023-12-06 17:01:21 +02:00
243f0f2b21 Update INSTALL.md 2023-12-06 16:52:47 +02:00
93b6d31505 s 2023-12-06 16:36:27 +02:00
429aed04b1 s 2023-12-06 16:32:53 +02:00
eeb20b055a feat: Add line count to file patch info and enhance PR description formatting 2023-12-06 15:29:45 +02:00
4b073b32a5 feat: Enhance PR description with file label dictionary and update prompts in pr_description_prompts.toml 2023-12-06 12:30:51 +02:00
f629755a9a feat: Refine field descriptions in pr_description_prompts.toml for semantic file labels 2023-12-06 10:59:44 +02:00
c1ed3ee511 feat: Refine field descriptions in pr_description_prompts.toml for semantic file labels 2023-12-06 08:08:01 +02:00
a4e6c99c82 Merge branch 'main' into hl/labeling_files 2023-12-05 08:28:59 -08:00
0b70e07b8c feat: Improve formatting in help.py command descriptions 2023-12-05 18:26:35 +02:00
862c236076 s 2023-12-05 18:10:13 +02:00
5c2f81a928 Merge pull request #508 from Codium-ai/trt/fix_describe
docs: Update issue link in INSTALL.md and enhance key formatting in p…
2023-12-05 07:01:07 -08:00
b76bc390f1 Merge pull request #503 from pzarfos/python312
Update dependencies in requirements.txt for Python 3.12
2023-12-05 06:55:19 -08:00
25d1e84b7f docs: Update issue link in INSTALL.md and enhance key formatting in pr_description.py 2023-12-05 16:54:18 +02:00
70a409abf1 Merge pull request #450 from koid/fix/specify-cache-directory-in-azure-devops
Specify Cache Directory in Azure DevOps in Lambda Function Setup
2023-12-05 06:48:44 -08:00
cf3401536a feat: Remove 'Refactoring' label from custom labels and update related descriptions 2023-12-05 07:48:21 +02:00
2feaee4306 feat: Update field descriptions in pr_description_prompts.toml for clarity 2023-12-04 21:45:22 +02:00
863eb0105d feat: Refactor semantic labels in PR description and improve clarity in pr_description.py and pr_description_prompts.toml 2023-12-04 21:23:59 +02:00
21a7a0f136 feat: Enhance link generation for relevant lines and refactor code in git providers and PR description tools 2023-12-04 21:06:56 +02:00
d2a129fe30 Add labeling files 2023-12-04 18:22:35 +02:00
fe796245a3 Merge pull request #501 from Codium-ai/tr/prompt_tuning
Refactoring and Enhancement of PR Agent Prompts
2023-12-04 03:18:12 -08:00
10bc84eb5b Merge pull request #505 from Codium-ai/mrT23-patch-1
Update Usage.md
2023-12-04 01:15:06 -08:00
06c0a35a65 Update Usage.md 2023-12-04 11:11:02 +02:00
082bcd00a1 Merge pull request #502 from pzarfos/local_models
Add descriptive error message for missing model in MAX_TOKENS array
2023-12-03 21:26:21 -08:00
71b421efa3 Updated requirements.txt for Python 3.12 2023-12-03 21:54:19 -05:00
317fec0536 Throw descriptive error message if model is not in MAX_TOKENS array 2023-12-03 21:06:55 -05:00
4dcbce41c8 feat: Refine prompts and improve formatting in pr_sort_code_suggestions_prompts.toml and pr_update_changelog_prompts.toml 2023-12-03 17:27:22 +02:00
b3fa654446 feat: Refactor prompts and improve formatting in pr_questions_prompts.toml 2023-12-03 17:23:52 +02:00
e09439fc1b feat: Enhance formatting and clarity in pr_information_from_user_prompts.toml 2023-12-03 17:17:24 +02:00
324e481ce7 feat: Improve formatting and clarity in pr_custom_labels.toml 2023-12-03 17:15:29 +02:00
abfad088e3 feat: Refine prompts and instructions in pr_add_docs.toml 2023-12-03 17:10:33 +02:00
f30789e6c8 feat: Refactor and enhance prompts in pr_code_suggestions_prompts.toml 2023-12-03 16:59:47 +02:00
5c01f97f54 feat: Enhance PR description prompts in pr_description_prompts.toml 2023-12-03 16:48:26 +02:00
2d726edbe4 feat: Improve formatting and clarity in pr_reviewer_prompts.toml 2023-12-03 16:40:06 +02:00
526ad00812 Merge pull request #500 from Codium-ai/tr/describe_message
Enhance pr_url assignment in github_provider.py for GitHub Actions compatibility
2023-12-03 02:05:38 -08:00
37812dfede feat: Update pr_url assignment in github_provider.py for GitHub Actions compatibility 2023-12-03 11:34:17 +02:00
c9debc38f2 Merge pull request #499 from Codium-ai/tr/describe_message
final update message in PR description
2023-12-03 01:02:21 -08:00
fe7d2bb924 update docs 2023-12-03 10:52:00 +02:00
586785ffde feat: Add pr_url attribute to git providers and final update message in PR description 2023-12-03 10:46:02 +02:00
c21e606eee Merge pull request #498 from Codium-ai/tr/update_pics
Update Image URLs in Markdown Documentation
2023-12-03 00:11:13 -08:00
a658766046 Merge pull request #495 from Codium-ai/tr/review_protection
Enhanced Exception Handling and Code Suggestion Formatting
2023-12-03 00:11:02 -08:00
3f2175e548 diagram 2023-12-03 09:12:10 +02:00
3ff30fcc92 fix logo for non-github image 2023-12-03 08:58:28 +02:00
07abf4788c docs: Update image URLs in markdown files 2023-12-03 08:41:09 +02:00
492dd3c281 docs: Update Azure setup instructions in Usage.md 2023-12-03 08:25:12 +02:00
d2fb1cfce5 Merge pull request #497 from network-charles/network-charles-patch-1
Update pr-agent/docs/Review.md Image
2023-12-02 22:18:56 -08:00
24fe5a572a Update REVIEW.md 2023-12-02 10:36:37 +01:00
3af9c3bfb9 feat: Enhance code suggestion publishing with language-specific formatting in pr_code_suggestions.py 2023-12-01 12:12:49 +02:00
c22084c7ac feat: Add exception handling for missing previous review in github_provider.py 2023-12-01 11:56:03 +02:00
96b91c9daa feat: Add dynaconf to requirements.txt 2023-11-30 18:29:27 +02:00
55464d5c5b Merge pull request #492 from Codium-ai/mrT23-patch-1
Update IMPROVE.md
2023-11-30 18:19:36 +02:00
f6048e8157 Merge pull request #490 from network-charles/network-charles-patch-1
Alphabetical Reordering of requirements.txt
2023-11-30 08:16:01 -08:00
e474982485 Merge pull request #491 from network-charles/network-charles-patch-2
Update INSTALL.md
2023-11-30 07:18:48 -08:00
18f06cc670 Update IMPROVE.md 2023-11-30 17:13:47 +02:00
574e3b9d32 Update IMPROVE.md 2023-11-30 16:35:44 +02:00
f7410da330 Update INSTALL.md 2023-11-30 14:13:20 +01:00
76f3d54519 Update INSTALL.md 2023-11-30 13:56:22 +01:00
59f117d916 Update INSTALL.md 2023-11-29 16:07:57 +01:00
4a71259be7 Update requirements.txt 2023-11-29 15:47:53 +01:00
b90dde48c0 Merge pull request #483 from tmokmss/add-bedrock-support
Add Amazon Bedrock support
2023-11-29 03:08:01 -08:00
c57807e53a Merge pull request #489 from Codium-ai/ok/gh_actions_fix
Improve error handling in settings retrieval
2023-11-29 11:55:22 +02:00
0e54a13272 Improve error handling in settings retrieval
Fix bug where default values were not being used in GitHub Action runners when environmental variables are not set. Now, if an environmental variable cannot be found and no default is given, the default value will be used if one exists. This will prevent errors during setup on different environments and ensure system stability.
2023-11-29 11:52:02 +02:00
ddc6c02018 Merge pull request #488 from Codium-ai/ok/gh_actions_fix
"Add fallback to environment variables in GitHub Action Runner"
2023-11-29 11:49:40 +02:00
b67d06ae59 "Add fallback to environment variables in GitHub Action Runner"
A new function `get_setting_or_env` was implemented to facilitate fetching of either settings or environmental variables in the GitHub Action Runner. This was necessary to resolve an issue where a certain undefined behaviour occurs in GitHub Actions, leading to an attribute error. The new function also provides a default value parameter to ensure the return of a value even in failed attempts to fetch from either settings or environment variables.
2023-11-29 11:47:52 +02:00
ca1289af03 Update pr-agent-review.yaml 2023-11-29 11:03:24 +02:00
5e642c10fa fallback to try_fix_yaml 2023-11-29 17:57:54 +09:00
b5a643d67a Merge pull request #487 from Codium-ai/ok/gh_actions_fix
Add utility function to handle boolean type conversion
2023-11-29 10:38:39 +02:00
580eede021 Add utility function to handle boolean type conversion
A utility function (`is_true`) was added to take care of validating and converting boolean values from string or boolean types. This function is used in three parts of the `run_action` method where automatic PR review, description, and improvement actions are triggered based on environment settings. This change makes the condition checks cleaner and prevents code duplication.
2023-11-29 10:33:12 +02:00
ea56910a2f Merge pull request #485 from Codium-ai/tr/bitbucket
Enhancement of Inline Comment Publishing in Bitbucket Provider and Logging Addition
2023-11-28 08:35:40 -08:00
51e1278cd7 feat: Enhance inline comment publishing in Bitbucket provider and add logging for no suggestions in pr_code_suggestions.py 2023-11-28 18:29:35 +02:00
ddeb4b598d Merge pull request #484 from Codium-ai/tr/review_fix_tags
Refactor: Improved label handling in pr_reviewer.py
2023-11-28 07:14:29 -08:00
7e029ead45 refactor: Improve label handling in pr_reviewer.py 2023-11-28 17:03:55 +02:00
f8f57419c4 Update ai_handler.py 2023-11-28 23:08:17 +09:00
917f4b6a01 hard code value 2023-11-28 20:59:21 +09:00
97d6fb999a set max_tokens_to_sample 2023-11-28 20:58:57 +09:00
1373ca23fc support Amazon Bedrock 2023-11-28 20:58:42 +09:00
4521077433 Merge pull request #481 from Codium-ai/ok/fix_improve
Refactoring Environment Variables Access
2023-11-27 23:31:47 -08:00
6264624c05 Merge branch 'main' into ok/fix_improve 2023-11-27 07:27:25 -08:00
5f6fa5a082 Update Usage.md 2023-11-27 16:18:02 +02:00
2dcee63df5 fix improve 2023-11-27 12:32:53 +02:00
c6cc676275 Update pr-agent-review.yaml 2023-11-27 08:22:58 +02:00
b8e4d10b9d summarize=true 2023-11-27 07:36:02 +02:00
70a957caf0 AUTO_IMPROVE 2023-11-27 07:35:17 +02:00
5ff9aaedfd Update IMPROVE.md 2023-11-26 17:41:20 +02:00
fc8865f8dc Merge pull request #476 from Codium-ai/tr/improve_inplace
Enhancements and Bug Fixes in Code Suggestions and Line Link Generation
2023-11-26 07:32:37 -08:00
466af37675 s 2023-11-26 17:21:02 +02:00
2202ff1cdf s 2023-11-26 17:17:36 +02:00
2022018d4c update tests 2023-11-26 17:16:04 +02:00
b1c374808d feat: Add line link generation in Bitbucket provider and improve markdown formatting in pr_code_suggestions.py and IMRPOVE.md 2023-11-26 17:12:02 +02:00
20978402ea s 2023-11-26 16:56:06 +02:00
8f615e17a3 s 2023-11-26 16:42:41 +02:00
5cbbaf44c9 feat: Add line link generation for GitLab and improve markdown formatting in pr_code_suggestions.py 2023-11-26 13:42:57 +02:00
f96d4924e7 feat: Add line link generation in git providers and refactor code suggestions generation 2023-11-26 11:57:45 +02:00
f36b672eaa feat: Add option to summarize code suggestions in pr_code_suggestions.py 2023-11-26 11:22:14 +02:00
f104b70703 Update INSTALL.md 2023-11-26 10:38:49 +02:00
d4e979cb02 Merge pull request #447 from Codium-ai/tr/pydantic
Refactor PR label handling and update CLI commands
2023-11-25 23:37:02 -08:00
668041c09f Code suggestions guidelines: 2023-11-26 09:32:02 +02:00
aa73eb2841 PR 2023-11-26 09:24:33 +02:00
14d4ca8c74 PR 2023-11-26 09:22:19 +02:00
690c113603 refactor: Improve clarity and consistency in pr_code_suggestions_prompts.toml and pr_reviewer_prompts.toml files 2023-11-26 09:17:42 +02:00
1a28c77783 Previous description 2023-11-26 09:08:33 +02:00
0326b7e4ac refactor: Update PR prompts in toml files for clarity and consistency 2023-11-26 09:05:45 +02:00
d8ae32fc55 language_extension_map 2023-11-26 08:52:55 +02:00
8db2e3b2a0 feat: Enhance readability in toml files and add verbosity level logging in pr_generate_labels.py 2023-11-26 08:42:04 +02:00
9465b7b577 refactor: Move clip_tokens function from pr_processing to utils module, and add tests 2023-11-26 08:29:47 +02:00
d7df4287f8 feat: Update PR prompts in toml files to enhance readability and consistency 2023-11-26 08:17:16 +02:00
b3238e90f2 s 2023-11-26 08:10:01 +02:00
fdfd6247fb Merge branch 'main' into tr/pydantic 2023-11-25 21:36:16 -08:00
46d4d04e94 Merge pull request #455 from lukefx/bitbucket-server
Added BitBucket Server and Data Center support
2023-11-25 21:33:26 -08:00
0f6564f42d feat: Added server and documentation 2023-11-25 17:37:44 +01:00
cddf183e03 Merge pull request #470 from Codium-ai/tr/glob
Enhance glob pattern handling and logging in file filtering
2023-11-22 23:19:09 -08:00
e80a0ed9c8 glob 2023-11-23 09:16:50 +02:00
d6d362b51e Merge pull request #469 from Codium-ai/mrT23-patch-1
Improve Documentation in Usage.md
2023-11-22 22:14:22 -08:00
4eff0282a1 Update Usage.md 2023-11-23 08:06:07 +02:00
8fc07df6ef Update INSTALL.md 2023-11-21 18:39:36 +02:00
84e4b607cc Merge pull request #467 from Codium-ai/ok/base_url
Add support for base_url in GitHub SDK
2023-11-21 16:51:16 +02:00
613ccb4c34 Add support for base_url in GitHub SDK 2023-11-21 16:48:36 +02:00
e95a6a8b07 Merge pull request #466 from Codium-ai/ok/gitlab_fix
Fix a bug in GitLab webhook
2023-11-21 16:36:40 +02:00
2add584fbc Fix a bug in GitLab webhook 2023-11-21 16:28:01 +02:00
54d7d59177 Update Usage.md 2023-11-20 20:06:07 +02:00
b3129c7dd9 Merge pull request #464 from Codium-ai/tr/more_protections
Refactor YAML parsing for improved error handling
2023-11-20 02:28:57 -08:00
3f76d95495 ScannerError 2023-11-20 10:35:35 +02:00
1b600cd85f Refactor YAML parsing for improved error handling 2023-11-20 10:30:59 +02:00
26cc26129c Merge pull request #463 from Codium-ai/tr/more_protections
minor fix
2023-11-19 07:45:39 -08:00
d1d7903e39 minor fix 2023-11-19 17:44:11 +02:00
dff4d1befc Merge pull request #462 from Codium-ai/tr/more_protections
Enhancements in YAML Parsing and Error Handling
2023-11-19 07:40:06 -08:00
3504a64269 protections 2023-11-19 17:35:40 +02:00
83247cadec protections 2023-11-19 17:30:57 +02:00
5ca1748b93 Merge pull request #460 from Codium-ai/tr/update_instructions
GFM mode for 'review' instructions
2023-11-19 01:20:36 -08:00
c7a681038d gfm instructions 2023-11-19 11:11:11 +02:00
eb977b4c24 gfm instructions 2023-11-19 11:02:11 +02:00
b62e0967d5 fix: Revert back to exception since context.get will not throw KeyError 2023-11-17 10:08:40 +01:00
14a934b146 Update Usage.md 2023-11-17 10:41:52 +02:00
26dc2e9d21 fix: raising exception instead of empty string
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-16 11:19:46 +01:00
d78a71184d fix: Use checked exception KeyError for missing key
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-16 10:59:01 +01:00
eae30c32a2 fix: Use checked exception for ValueError
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-16 10:58:35 +01:00
bc28d657b2 Merge pull request #438 from koid/fix/remove-unnecessary-setup
Removal of Redundant Logger Setup
2023-11-15 10:35:13 -08:00
3fae5cbd8d feat: Added BitBucket Server
Signed-off-by: Luca Simone <info@lucasimone.info>
2023-11-15 15:47:44 +01:00
172b5f0787 add instructions to lambda function section 2023-11-15 13:22:07 +09:00
0df0542958 prompt 2023-11-13 15:55:35 +02:00
a8dddd1999 prompt 2023-11-13 12:14:18 +02:00
f5c6dd55b8 triple quote 2023-11-13 12:04:58 +02:00
0e932af2e3 multi line 2023-11-13 12:01:08 +02:00
e9891fc530 s1 2023-11-12 16:37:53 +02:00
727eea2b62 s1 2023-11-12 15:00:06 +02:00
f0991526b5 remove unnecessary setup_logger 2023-11-08 16:56:44 +09:00
89 changed files with 1638 additions and 530 deletions

View File

@ -27,5 +27,6 @@ jobs:
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }} PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
GITHUB_ACTION.AUTO_REVIEW: true GITHUB_ACTION.AUTO_REVIEW: true
GITHUB_ACTION.AUTO_IMPROVE: true

View File

@ -1,2 +1,6 @@
[pr_reviewer] [pr_reviewer]
enable_review_labels_effort = true enable_review_labels_effort = true
[pr_code_suggestions]
summarize=true

View File

@ -25,6 +25,7 @@ There are several ways to use PR-Agent:
**BitBucket specific methods** **BitBucket specific methods**
- [Run as a Bitbucket Pipeline](INSTALL.md#run-as-a-bitbucket-pipeline) - [Run as a Bitbucket Pipeline](INSTALL.md#run-as-a-bitbucket-pipeline)
- [Run on a hosted app](INSTALL.md#run-on-a-hosted-bitbucket-app) - [Run on a hosted app](INSTALL.md#run-on-a-hosted-bitbucket-app)
- [Bitbucket server and data center](INSTALL.md#bitbucket-server-and-data-center)
--- ---
### Use Docker image (no installation required) ### Use Docker image (no installation required)
@ -101,6 +102,7 @@ python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
python3 -m pr_agent.cli --pr_url <pr_url> describe python3 -m pr_agent.cli --pr_url <pr_url> describe
python3 -m pr_agent.cli --pr_url <pr_url> improve python3 -m pr_agent.cli --pr_url <pr_url> improve
python3 -m pr_agent.cli --pr_url <pr_url> add_docs python3 -m pr_agent.cli --pr_url <pr_url> add_docs
python3 -m pr_agent.cli --pr_url <pr_url> generate_labels
python3 -m pr_agent.cli --issue_url <issue_url> similar_issue python3 -m pr_agent.cli --issue_url <issue_url> similar_issue
... ...
``` ```
@ -155,10 +157,11 @@ jobs:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 and variables > Actions > New repository secret > Add secret`:
``` ```
OPENAI_KEY: <your key> Name = OPENAI_KEY
Secret = <your key>
``` ```
The GITHUB_TOKEN secret is automatically created by GitHub. The GITHUB_TOKEN secret is automatically created by GitHub.
@ -203,6 +206,7 @@ Allowing you to automate the review process on your private or public repositori
- Set the following events: - Set the following events:
- Issue comment - Issue comment
- Pull request - Pull request
- Push (if you need to enable triggering on PR update)
2. Generate a random secret for your app, and save it for later. For example, you can use: 2. Generate a random secret for your app, and save it for later. For example, you can use:
@ -289,7 +293,8 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
``` ```
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m. 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. 5. Configure the lambda function to have a Function URL.
6. Go back to steps 8-9 of [Method 5](#run-as-a-github-app) with the function url as your Webhook URL. 6. In the environment variables of the Lambda function, specify `AZURE_DEVOPS_CACHE_DIR` to a writable location such as /tmp. (see [link](https://github.com/Codium-ai/pr-agent/pull/450#issuecomment-1840242269))
7. Go back to steps 8-9 of [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` The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks`
--- ---
@ -409,10 +414,49 @@ BITBUCKET_BEARER_TOKEN: <your token>
You can get a Bitbucket token for your repository by following Repository Settings -> Security -> Access Tokens. You can get a Bitbucket token for your repository by following Repository Settings -> Security -> Access Tokens.
Note that comments on a PR are not supported in Bitbucket Pipeline.
### Run using CodiumAI-hosted Bitbucket app ### Run using CodiumAI-hosted Bitbucket app
Please contact <support@codium.ai> or visit [CodiumAI pricing page](https://www.codium.ai/pricing/) if you're interested in a hosted BitBucket app solution that provides full functionality including PR reviews and comment handling. It's based on the [bitbucket_app.py](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/git_providers/bitbucket_provider.py) implementation. Please contact <support@codium.ai> or visit [CodiumAI pricing page](https://www.codium.ai/pricing/) if you're interested in a hosted BitBucket app solution that provides full functionality including PR reviews and comment handling. It's based on the [bitbucket_app.py](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/git_providers/bitbucket_provider.py) implementation.
### Bitbucket Server and Data Center
Login into your on-prem instance of Bitbucket with your service account username and password.
Navigate to `Manage account`, `HTTP Access tokens`, `Create Token`.
Generate the token and add it to .secret.toml under `bitbucket_server` section
```toml
[bitbucket_server]
bearer_token = "<your key>"
```
#### Run it as CLI
Modify `configuration.toml`:
```toml
git_provider="bitbucket_server"
```
and pass the Pull request URL:
```shell
python cli.py --pr_url https://git.onpreminstanceofbitbucket.com/projects/PROJECT/repos/REPO/pull-requests/1 review
```
#### Run it as service
To run pr-agent as webhook, build the docker image:
```
docker build . -t codiumai/pr-agent:bitbucket_server_webhook --target bitbucket_server_webhook -f docker/Dockerfile
docker push codiumai/pr-agent:bitbucket_server_webhook # Push to your Docker repository
```
Navigate to `Projects` or `Repositories`, `Settings`, `Webhooks`, `Create Webhook`.
Fill the name and URL, Authentication None select the Pull Request Opened checkbox to receive that event as webhook.
The URL should end with `/webhook`, for example: https://domain.com/webhook
======= =======

View File

@ -39,4 +39,4 @@ We use [tiktoken](https://github.com/openai/tiktoken) to tokenize the patches af
4. If we haven't reached the max token length, add the `deleted files` to the prompt until the prompt reaches the max token length (hard stop), skip the rest of the patches. 4. If we haven't reached the max token length, add the `deleted files` to the prompt until the prompt reaches the max token length (hard stop), skip the rest of the patches.
### Example ### Example
![](https://codium.ai/images/git_patch_logic.png) <kbd><img src=https://codium.ai/images/git_patch_logic.png width="768"></kbd>

View File

@ -2,8 +2,13 @@
<div align="center"> <div align="center">
<img src="./pics/logo-dark.png#gh-dark-mode-only" width="330"/>
<img src="./pics/logo-light.png#gh-light-mode-only" width="330"/><br/> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://codium.ai/images/pr_agent/logo-dark.png" width="330">
<source media="(prefers-color-scheme: light)" srcset="https://codium.ai/images/pr_agent/logo-light.png" width="330">
<img alt="logo">
</picture>
<br/>
Making pull requests less painful with an AI agent Making pull requests less painful with an AI agent
</div> </div>
@ -16,7 +21,7 @@ Making pull requests less painful with an AI agent
</div> </div>
<div style="text-align:left;"> <div style="text-align:left;">
CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of commands: CodiumAI `PR-Agent` is an open-source tool for efficient pull request reviewing and handling. It automatically analyzes the pull request and can provide several types of commands:
**Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels. **Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels.
\ \
@ -28,17 +33,17 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
\ \
**Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes. **Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes.
\ \
**Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues.
\ \
**Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR. **Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR.
\ \
**Generate Custom Labels ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes. **Generate Custom Labels ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes.
See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms. See the [Installation Guide](./INSTALL.md) for instructions on installing and running the tool on different git platforms.
See the [Usage Guide](./Usage.md) for instructions how to run the different tools from _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened. See the [Usage Guide](./Usage.md) for running the PR-Agent commands via different interfaces, including _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened.
See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools. See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools (tools are run via the commands).
<h3>Example results:</h3> <h3>Example results:</h3>
</div> </div>
@ -135,7 +140,7 @@ Review the [usage guide](./Usage.md) section for detailed instructions how to us
## Try it now ## Try it now
You can try GPT-4 powered PR-Agent, on your public GitHub repository, instantly. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command. Try the GPT-4 powered PR-Agent instantly on _your public GitHub repository_. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command.
For example, add a comment to any pull request with the following text: For example, add a comment to any pull request with the following text:
``` ```
@CodiumAI-Agent /review @CodiumAI-Agent /review
@ -146,6 +151,7 @@ and the agent will respond with a review of your PR
To set up your own PR-Agent, see the [Installation](#installation) section below. To set up your own PR-Agent, see the [Installation](#installation) section below.
Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there is no need to mention `@CodiumAI-Agent ...`. Instead, directly start with the command, e.g., `/ask ...`.
--- ---
@ -174,7 +180,7 @@ There are several ways to use PR-Agent:
The following diagram illustrates PR-Agent tools and their flow: The following diagram illustrates PR-Agent tools and their flow:
![PR-Agent Tools](https://www.codium.ai/wp-content/uploads/2023/10/codiumai-diagram-v5.png) ![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png)
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 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
@ -220,9 +226,12 @@ See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes.
## Data Privacy ## Data Privacy
If you use self-host PR-Agent, e.g. via CLI running on your computer, with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here: If you use a self-hosted PR-Agent with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here:
https://openai.com/enterprise-privacy https://openai.com/enterprise-privacy
When using a PR-Agent app hosted by CodiumAI, we will not store any of your data, nor will we used it for training.
You will also benefit from an OpenAI account with zero data retention.
## Links ## Links
[![Join our Discord community](https://raw.githubusercontent.com/Codium-ai/codiumai-vscode-release/main/media/docs/Joincommunity.png)](https://discord.gg/kG35uSHDBc) [![Join our Discord community](https://raw.githubusercontent.com/Codium-ai/codiumai-vscode-release/main/media/docs/Joincommunity.png)](https://discord.gg/kG35uSHDBc)

View File

@ -1,3 +1,21 @@
## [Version 0.11] - 2023-12-07
- codiumai/pr-agent:0.11
- codiumai/pr-agent:0.11-github_app
- codiumai/pr-agent:0.11-bitbucket-app
- codiumai/pr-agent:0.11-gitlab_webhook
- codiumai/pr-agent:0.11-github_polling
- codiumai/pr-agent:0.11-github_action
### Added::Algo
- New section in `/describe` tool - [PR changes walkthrough](https://github.com/Codium-ai/pr-agent/pull/509)
- Improving PR Agent [prompts](https://github.com/Codium-ai/pr-agent/pull/501)
- Persistent tools (`/review`, `/describe`) now send an [update message](https://github.com/Codium-ai/pr-agent/pull/499) after finishing
- Add Amazon Bedrock [support](https://github.com/Codium-ai/pr-agent/pull/483)
### Fixed
- Update [dependencies](https://github.com/Codium-ai/pr-agent/pull/503) in requirements.txt for Python 3.12
## [Version 0.10] - 2023-11-15 ## [Version 0.10] - 2023-11-15
- codiumai/pr-agent:0.10 - codiumai/pr-agent:0.10
- codiumai/pr-agent:0.10-github_app - codiumai/pr-agent:0.10-github_app

View File

@ -6,6 +6,7 @@
- [Online usage](#online-usage) - [Online usage](#online-usage)
- [Working with GitHub App](#working-with-github-app) - [Working with GitHub App](#working-with-github-app)
- [Working with GitHub Action](#working-with-github-action) - [Working with GitHub Action](#working-with-github-action)
- [Working with BitBucket App](#working-with-bitbucket-self-hosted-app)
- [Changing a model](#changing-a-model) - [Changing a model](#changing-a-model)
- [Working with large PRs](#working-with-large-prs) - [Working with large PRs](#working-with-large-prs)
- [Appendix - additional configurations walkthrough](#appendix---additional-configurations-walkthrough) - [Appendix - additional configurations walkthrough](#appendix---additional-configurations-walkthrough)
@ -32,12 +33,19 @@ The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the
#### Ignoring files from analysis #### Ignoring files from analysis
In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code. In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code.
To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting is also exposed the following environment variables: To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting also exposes the following environment variables:
- `IGNORE.GLOB` - `IGNORE.GLOB`
- `IGNORE.REGEX` - `IGNORE.REGEX`
See [dynaconf envvars documentation](https://www.dynaconf.com/envvars/). For example, to ignore python files in a PR with online usage, comment on a PR:
`/review --ignore.glob=['*.py']`
To ignore python files in all PRs, set in a configuration file:
```
[ignore]
glob = ['*.py']
```
#### git provider #### git provider
The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported: The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported:
@ -59,7 +67,7 @@ The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configu
### Working from a local repo (CLI) ### Working from a local repo (CLI)
When running from your local repo (CLI), your local configuration file will be used. When running from your local repo (CLI), your local configuration file will be used.
Examples for invoking the different tools via the CLI: Examples of invoking the different tools via the CLI:
- **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review` - **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review`
- **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe` - **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe`
@ -83,7 +91,7 @@ python -m pr_agent.cli --pr_url=<pr_url> /review --pr_reviewer.extra_instructio
publish_output=true publish_output=true
verbosity_level=2 verbosity_level=2
``` ```
This is useful for debugging or experimenting with the different tools. This is useful for debugging or experimenting with different tools.
### Online usage ### Online usage
@ -100,17 +108,17 @@ Commands for invoking the different tools via comments:
To edit a specific configuration value, just add `--config_path=<value>` to any command. To edit a specific configuration value, just add `--config_path=<value>` to any command.
For example if you want to edit the `review` tool configurations, you can run: For example, if you want to edit the `review` tool configurations, you can run:
``` ```
/review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false /review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false
``` ```
Any configuration value in [configuration file](pr_agent/settings/configuration.toml) file can be similarly edited. comment `/config` to see the list of available configurations. Any configuration value in [configuration file](pr_agent/settings/configuration.toml) file can be similarly edited. Comment `/config` to see the list of available configurations.
### Working with GitHub App ### Working with GitHub App
When running PR-Agent from GitHub App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded. When running PR-Agent from GitHub App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
By uploading a local `.pr_agent.toml` file, you can edit and customize any configuration parameter. By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
For example, if you set in `.pr_agent.toml`: For example, if you set in `.pr_agent.toml`:
@ -119,7 +127,7 @@ For example, if you set in `.pr_agent.toml`:
num_code_suggestions=1 num_code_suggestions=1
``` ```
Than you will overwrite the default number of code suggestions to be 1. Then you will overwrite the default number of code suggestions to 1.
#### GitHub app automatic tools #### GitHub app automatic tools
The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations. The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations.
@ -133,7 +141,7 @@ The GitHub app can respond to the following actions on a PR:
4. `review_requested` - Specifically requesting review (in the PR reviewers list) from the `github-actions[bot]` user 4. `review_requested` - Specifically requesting review (in the PR reviewers list) from the `github-actions[bot]` user
The configuration parameter `handle_pr_actions` defines the list of actions for which the GitHub app will trigger the PR-Agent. The configuration parameter `handle_pr_actions` defines the list of actions for which the GitHub app will trigger the PR-Agent.
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when one of the above action happens (e.g. a new PR is opened): The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when one of the above actions happens (e.g., a new PR is opened):
``` ```
[github_app] [github_app]
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested'] handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
@ -173,11 +181,11 @@ push_commands = [
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true", "/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
] ]
``` ```
The means that when new code is pushed to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools. This means that when new code is pushed to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools.
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true. For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
For the `auto_review` tool, it will run in incremental mode, and the `remove_previous_review_comment` parameter will be set to true. For the `auto_review` tool, it will run in incremental mode, and the `remove_previous_review_comment` parameter will be set to true.
Much like the configurations for `pr_commands`, you can override the default tool paramteres by uploading a local configuration file to the root of your repo. Much like the configurations for `pr_commands`, you can override the default tool parameters by uploading a local configuration file to the root of your repo.
#### Editing the prompts #### Editing the prompts
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder. The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder.
@ -218,6 +226,35 @@ For example, you can set an environment variable: `pr_description.add_original_u
add_original_user_description = false add_original_user_description = false
``` ```
### Working with BitBucket Self-Hosted App
Similar to GitHub app, when running PR-Agent from BitBucket App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
For example, if your local `.pr_agent.toml` file contains:
```
[pr_reviewer]
inline_code_comments = true
```
Each time you invoke a `/review` tool, it will use inline code comments.
#### BitBucket Self-Hosted App automatic tools
You can configure in your local `.pr_agent.toml` file which tools will **run automatically** when a new PR is opened.
Specifically, set the following values:
```yaml
[bitbucket_app]
auto_review = true # set as config var in .pr_agent.toml
auto_describe = true # set as config var in .pr_agent.toml
auto_improve = true # set as config var in .pr_agent.toml
```
`bitbucket_app.auto_review`, `bitbucket_app.auto_describe` and `bitbucket_app.auto_improve` are used to enable/disable automatic tools.
If not set, the default option is that only the `review` tool will run automatically when a new PR is opened.
Note that due to limitations of the bitbucket platform, the `auto_describe` tool will be able to publish a PR description only as a comment.
In addition, some subsections like `PR changes walkthrough` will not appear, since they require the usage of collapsible sections, which are not supported by bitbucket.
### Changing a model ### Changing a model
@ -226,7 +263,7 @@ To use a different model than the default (GPT-4), you need to edit [configurati
For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions. For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions.
#### Azure #### Azure
To use Azure, set in your .secrets.toml: To use Azure, set in your `.secrets.toml` (working from CLI), or in the GitHub `Settings > Secrets and variables` (working from GitHub App or GitHub Action):
``` ```
api_key = "" # your azure api key api_key = "" # your azure api key
api_type = "azure" api_type = "azure"
@ -235,12 +272,11 @@ api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<you
openai.deployment_id = "" # The deployment name you chose when you deployed the engine openai.deployment_id = "" # The deployment name you chose when you deployed the engine
``` ```
and and set in your configuration file:
``` ```
[config] [config]
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo) model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
``` ```
in the configuration.toml
#### Huggingface #### Huggingface
@ -256,7 +292,7 @@ MAX_TOKENS = {
e.g. e.g.
MAX_TOKENS={ MAX_TOKENS={
..., ...,
"llama2": 4096 "ollama/llama2": 4096
} }
@ -265,6 +301,8 @@ model = "ollama/llama2"
[ollama] # in .secrets.toml [ollama] # in .secrets.toml
api_base = ... # the base url for your huggingface inference endpoint api_base = ... # the base url for your huggingface inference endpoint
# e.g. if running Ollama locally, you may use:
api_base = "http://localhost:11434/"
``` ```
**Inference Endpoints** **Inference Endpoints**
@ -310,6 +348,7 @@ To use Google's Vertex AI platform and its associated models (chat-bison/codecha
``` ```
[config] # in configuration.toml [config] # in configuration.toml
model = "vertex_ai/codechat-bison" model = "vertex_ai/codechat-bison"
fallback_models="vertex_ai/codechat-bison"
[vertexai] # in .secrets.toml [vertexai] # in .secrets.toml
vertex_project = "my-google-cloud-project" vertex_project = "my-google-cloud-project"
@ -320,6 +359,23 @@ Your [application default credentials](https://cloud.google.com/docs/authenticat
If you do want to set explicit credentials then you can use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable set to a path to a json credentials file. If you do want to set explicit credentials then you can use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable set to a path to a json credentials file.
#### Amazon Bedrock
To use Amazon Bedrock and its foundational models, add the below configuration:
```
[config] # in configuration.toml
model = "anthropic.claude-v2"
fallback_models="anthropic.claude-instant-v1"
[aws] # in .secrets.toml
bedrock_region = "us-east-1"
```
Note that you have to add access to foundational models before using them. Please refer to [this document](https://docs.aws.amazon.com/bedrock/latest/userguide/setting-up.html) for more details.
AWS session is automatically authenticated from your environment, but you can also explicitly set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.
### Working with large PRs ### Working with large PRs
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens. The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.

View File

@ -14,6 +14,10 @@ FROM base as bitbucket_app
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_app.py"] CMD ["python", "pr_agent/servers/bitbucket_app.py"]
FROM base as bitbucket_server_webhook
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_server_webhook.py"]
FROM base as github_polling FROM base as github_polling
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"] CMD ["python", "pr_agent/servers/github_polling.py"]

View File

@ -7,8 +7,8 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=./../pics/add_docs_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/add_docs_comment.png width="768"></kbd>
<kbd><img src=./../pics/add_docs.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/add_docs.png width="768"></kbd>
### Configuration options ### Configuration options
- `docs_style`: The exact style of the documentation (for python docstring). you can choose between: `google`, `numpy`, `sphinx`, `restructuredtext`, `plain`. Default is `sphinx`. - `docs_style`: The exact style of the documentation (for python docstring). you can choose between: `google`, `numpy`, `sphinx`, `restructuredtext`, `plain`. Default is `sphinx`.

View File

@ -7,5 +7,5 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=./../pics/ask_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/ask_comment.png width="768"></kbd>
<kbd><img src=./../pics/ask.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/ask.png width="768"></kbd>

View File

@ -1,15 +1,15 @@
# Describe Tool # Describe Tool
The `describe` tool scans the PR code changes, and automatically generates PR description - title, type, summary, code walkthrough and labels. The `describe` tool scans the PR code changes, and automatically generates PR description - title, type, summary, walkthrough and labels.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
/describe /describe
``` ```
For example: For example:
<kbd><img src=./../pics/describe_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/describe_comment.png width="768"></kbd>
<kbd><img src=./../pics/describe.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/describe.png width="768"></kbd>
The `describe` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action) The `describe` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
@ -26,9 +26,15 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti
- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false. - `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...". - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes) - To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
- `enable_pr_type`: if set to false, it will not show the `PR type` as a text value in the description content. Default is true. - `enable_pr_type`: if set to false, it will not show the `PR type` as a text value in the description content. Default is true.
- `final_update_message`: if set to true, it will add a comment message [`PR Description updated to latest commit...`](https://github.com/Codium-ai/pr-agent/pull/499#issuecomment-1837412176) after finishing calling `/describe`. Default is true.
- `enable_semantic_files_types`: if set to true, "PR changes walkthrough" section will be generated. Default is true.
### Markers template ### Markers template
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism. markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
@ -52,11 +58,11 @@ The marker `pr_agent:summary` will be replaced with the PR summary, and `pr_agen
pr_description.use_description_markers: 'true' pr_description.use_description_markers: 'true'
``` ```
<kbd><img src=./../pics/describe_markers_before.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/describe_markers_before.png width="768"></kbd>
==> ==>
<kbd><img src=./../pics/describe_markers_after.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/describe_markers_after.png width="768"></kbd>
##### Configuration params: ##### Configuration params:

View File

@ -9,10 +9,10 @@ For example:
If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description: If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description:
<kbd><img src=./../pics/custom_labels_list.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/custom_labels_list.png width="768"></kbd>
When running the `generate_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label: When running the `generate_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label:
<kbd><img src=./../pics/custom_label_published.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/custom_label_published.png width="768"></kbd>
### How to enable custom labels ### How to enable custom labels

View File

@ -1,14 +1,14 @@
# Improve Tool # Improve Tool
The `improve` tool scans the PR code changes, and automatically generate committable suggestions for improving the PR code. The `improve` tool scans the PR code changes, and automatically generates committable suggestions for improving the PR code.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
/improve /improve
``` ```
For example: For example:
<kbd><img src=./../pics/improve_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/improve_comment.png width="768"></kbd>
<kbd><img src=./../pics/improve.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/improve.png width="768"></kbd>
The `improve` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action) The `improve` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
@ -17,7 +17,7 @@ An extended mode, which does not involve PR Compression and provides more compre
/improve --extended /improve --extended
``` ```
Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (multiple calls to GPT-4). Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (multiple calls to GPT-4).
Hence, the total number of suggestions is proportional to the number of chunks, i.e. the size of the PR. Hence, the total number of suggestions is proportional to the number of chunks, i.e., the size of the PR.
### Configuration options ### Configuration options
@ -33,10 +33,22 @@ Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/
- `max_number_of_calls`: maximum number of chunks. Default is 5. - `max_number_of_calls`: maximum number of chunks. Default is 5.
- `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9. - `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9.
#### summarize mode
- `summarize`: if set to true, the tool will present the code suggestions in a compact way. Default is false.
#### A note on code suggestions quality In this mode, instead of presenting committable suggestions, the different suggestions will be combined into a single compact comment, with significantly smaller PR footprint.
- With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically. For example:
`/improve --pr_code_suggestions.summarize=true`
<kbd><img src=https://codium.ai/images/pr_agent/improved_summerize_open.png width="768"></kbd>
___
### A note on code suggestions quality
- While the current AI for code is getting better and better (GPT-4), it's not flawless. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base. - Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.

View File

@ -7,8 +7,8 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=./../pics/review_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/review_comment.png width="768"></kbd>
<kbd><img src=./../pics/describe.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/review.png width="768"></kbd>
The `review` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action) The `review` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
@ -40,7 +40,7 @@ For an incremental review, which only considers changes since the last PR-Agent
``` ```
Note that the incremental mode is only available for GitHub. Note that the incremental mode is only available for GitHub.
<kbd><img src=./../pics/incremental_review.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/incremental_review.png width="768"></kbd>
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review -i' tool. Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review -i' tool.
These configurations can be used to control the rate at which the incremental review tool will create new review comments when invoked automatically, to prevent making too much noise in the PR. These configurations can be used to control the rate at which the incremental review tool will create new review comments when invoked automatically, to prevent making too much noise in the PR.
@ -63,9 +63,9 @@ By invoking:
``` ```
The tool will first ask the author questions about the PR, and will guide the review based on his answers. The tool will first ask the author questions about the PR, and will guide the review based on his answers.
<kbd><img src=./../pics/reflection_questions.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/reflection_questions.png width="768"></kbd>
<kbd><img src=./../pics/reflection_answers.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/reflection_answers.png width="768"></kbd>
<kbd><img src=./../pics/reflection_insights.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/reflection_insights.png width="768"></kbd>
#### A note on code suggestions quality #### A note on code suggestions quality

View File

@ -6,9 +6,9 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=./../pics/similar_issue_original_issue.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/similar_issue_original_issue.png width="768"></kbd>
<kbd><img src=./../pics/similar_issue_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/similar_issue_comment.png width="768"></kbd>
<kbd><img src=./../pics/similar_issue.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/similar_issue.png width="768"></kbd>
Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once). Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once).

View File

@ -7,8 +7,8 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=./../pics/update_changelog_comment.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/update_changelog_comment.png width="768"></kbd>
<kbd><img src=./../pics/update_changelog.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/update_changelog.png width="768"></kbd>
### Configuration options ### Configuration options

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -18,4 +18,7 @@ MAX_TOKENS = {
'vertex_ai/codechat-bison-32k': 32000, 'vertex_ai/codechat-bison-32k': 32000,
'codechat-bison': 6144, 'codechat-bison': 6144,
'codechat-bison-32k': 32000, 'codechat-bison-32k': 32000,
'anthropic.claude-v2': 100000,
'anthropic.claude-instant-v1': 100000,
'anthropic.claude-v1': 100000,
} }

View File

@ -1,5 +1,6 @@
import os import os
import boto3
import litellm import litellm
import openai import openai
from litellm import acompletion from litellm import acompletion
@ -24,6 +25,7 @@ class AiHandler:
Raises a ValueError if the OpenAI key is missing. Raises a ValueError if the OpenAI key is missing.
""" """
self.azure = False self.azure = False
self.aws_bedrock_client = None
if get_settings().get("OPENAI.KEY", None): if get_settings().get("OPENAI.KEY", None):
openai.api_key = get_settings().openai.key openai.api_key = get_settings().openai.key
@ -60,6 +62,12 @@ class AiHandler:
litellm.vertex_location = get_settings().get( litellm.vertex_location = get_settings().get(
"VERTEXAI.VERTEX_LOCATION", None "VERTEXAI.VERTEX_LOCATION", None
) )
if get_settings().get("AWS.BEDROCK_REGION", None):
litellm.AmazonAnthropicConfig.max_tokens_to_sample = 2000
self.aws_bedrock_client = boto3.client(
service_name="bedrock-runtime",
region_name=get_settings().aws.bedrock_region,
)
@property @property
def deployment_id(self): def deployment_id(self):
@ -100,13 +108,16 @@ class AiHandler:
if self.azure: if self.azure:
model = 'azure/' + model model = 'azure/' + model
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
response = await acompletion( kwargs = {
model=model, "model": model,
deployment_id=deployment_id, "deployment_id": deployment_id,
messages=messages, "messages": messages,
temperature=temperature, "temperature": temperature,
force_timeout=get_settings().config.ai_timeout "force_timeout": get_settings().config.ai_timeout,
) }
if self.aws_bedrock_client:
kwargs["aws_bedrock_client"] = self.aws_bedrock_client
response = await acompletion(**kwargs)
except (APIError, Timeout, TryAgain) as e: except (APIError, Timeout, TryAgain) as e:
get_logger().error("Error during OpenAI inference: ", e) get_logger().error("Error during OpenAI inference: ", e)
raise raise

View File

@ -11,7 +11,12 @@ def filter_ignored(files):
try: try:
# load regex patterns, and translate glob patterns to regex # load regex patterns, and translate glob patterns to regex
patterns = get_settings().ignore.regex patterns = get_settings().ignore.regex
patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] if isinstance(patterns, str):
patterns = [patterns]
glob_setting = get_settings().ignore.glob
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
glob_setting = glob_setting.strip('[]').split(",")
patterns += [fnmatch.translate(glob) for glob in glob_setting]
# compile all valid patterns # compile all valid patterns
compiled_patterns = [] compiled_patterns = []

View File

@ -3,8 +3,7 @@ from typing import Dict
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
# Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501 # Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
bad_extensions = get_settings().bad_extensions.default bad_extensions = get_settings().bad_extensions.default
@ -29,6 +28,8 @@ def sort_files_by_main_languages(languages: Dict, files: list):
# languages_sorted = sorted(languages, key=lambda x: x[1], reverse=True) # languages_sorted = sorted(languages, key=lambda x: x[1], reverse=True)
# get all extensions for the languages # get all extensions for the languages
main_extensions = [] main_extensions = []
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
for language in languages_sorted_list: for language in languages_sorted_list:
if language.lower() in language_extension_map: if language.lower() in language_extension_map:
main_extensions.append(language_extension_map[language.lower()]) main_extensions.append(language_extension_map[language.lower()])

View File

@ -10,7 +10,7 @@ from github import RateLimitExceededException
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions 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.language_handler import sort_files_by_main_languages
from pr_agent.algo.file_filter import filter_ignored from pr_agent.algo.file_filter import filter_ignored
from pr_agent.algo.token_handler import TokenHandler, get_token_encoder from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import get_max_tokens from pr_agent.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
@ -326,35 +326,6 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
return position, absolute_position return position, absolute_position
def clip_tokens(text: str, max_tokens: int) -> str:
"""
Clip the number of tokens in a string to a maximum number of tokens.
Args:
text (str): The string to clip.
max_tokens (int): The maximum number of tokens allowed in the string.
Returns:
str: The clipped string.
"""
if not text:
return 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:
get_logger().warning(f"Failed to clip tokens: {e}")
return text
def get_pr_multi_diffs(git_provider: GitProvider, def get_pr_multi_diffs(git_provider: GitProvider,
token_handler: TokenHandler, token_handler: TokenHandler,
model: str, model: str,

View File

@ -11,6 +11,7 @@ import yaml
from starlette_context import context from starlette_context import context
from pr_agent.algo import MAX_TOKENS from pr_agent.algo import MAX_TOKENS
from pr_agent.algo.token_handler import get_token_encoder
from pr_agent.config_loader import get_settings, global_settings from pr_agent.config_loader import get_settings, global_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -57,14 +58,15 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
emoji = emojis.get(key, "") emoji = emojis.get(key, "")
if key.lower() == 'code feedback': if key.lower() == 'code feedback':
if gfm_supported: if gfm_supported:
markdown_text += f"\n\n- **<details><summary> { emoji } Code feedback:**</summary>\n\n" markdown_text += f"\n\n- "
markdown_text += f"<details><summary> { emoji } Code feedback:</summary>"
else: else:
markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n" markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n"
else: else:
markdown_text += f"- {emoji} **{key}:**\n\n" markdown_text += f"- {emoji} **{key}:**\n\n"
for item in value: for i, item in enumerate(value):
if isinstance(item, dict) and key.lower() == 'code feedback': if isinstance(item, dict) and key.lower() == 'code feedback':
markdown_text += parse_code_suggestion(item, gfm_supported) markdown_text += parse_code_suggestion(item, i, gfm_supported)
elif item: elif item:
markdown_text += f" - {item}\n" markdown_text += f" - {item}\n"
if key.lower() == 'code feedback': if key.lower() == 'code feedback':
@ -78,7 +80,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
return markdown_text return markdown_text
def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str: def parse_code_suggestion(code_suggestions: dict, i: int = 0, gfm_supported: bool = True) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
@ -89,24 +91,52 @@ def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> s
str: A string containing the markdown formatted text generated from the input dictionary. str: A string containing the markdown formatted text generated from the input dictionary.
""" """
markdown_text = "" markdown_text = ""
for sub_key, sub_value in code_suggestions.items(): if gfm_supported and 'relevant line' in code_suggestions:
if isinstance(sub_value, dict): # "code example" if i == 0:
markdown_text += f" - **{sub_key}:**\n" markdown_text += "<hr>"
for code_key, code_value in sub_value.items(): # 'before' and 'after' code markdown_text += '<table>'
code_str = f"```\n{code_value}\n```" for sub_key, sub_value in code_suggestions.items():
code_str_indented = textwrap.indent(code_str, ' ') try:
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n" if sub_key.lower() == 'relevant file':
else: relevant_file = sub_value.strip('`').strip('"').strip("'")
if "relevant file" in sub_key.lower(): markdown_text += f"<tr><td>{sub_key}</td><td>{relevant_file}</td></tr>"
markdown_text += f"\n - **{sub_key}:** {sub_value}\n" # continue
elif sub_key.lower() == 'suggestion':
markdown_text += f"<tr><td>{sub_key} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td><td><strong>{sub_value}</strong></td></tr>"
elif sub_key.lower() == 'relevant line':
markdown_text += f"<tr><td>relevant line</td>"
sub_value_list = sub_value.split('](')
relevant_line = sub_value_list[0].lstrip('`').lstrip('[')
if len(sub_value_list) > 1:
link = sub_value_list[1].rstrip(')').strip('`')
markdown_text += f"<td><a href={link}>{relevant_line}</a></td>"
else:
markdown_text += f"<td>{relevant_line}</td>"
markdown_text += "</tr>"
except Exception as e:
get_logger().exception(f"Failed to parse code suggestion: {e}")
pass
markdown_text += '</table>'
markdown_text += "<hr>"
else:
for sub_key, sub_value in code_suggestions.items():
if isinstance(sub_value, dict): # "code example"
markdown_text += f" - **{sub_key}:**\n"
for code_key, code_value in sub_value.items(): # 'before' and 'after' code
code_str = f"```\n{code_value}\n```"
code_str_indented = textwrap.indent(code_str, ' ')
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
else: else:
markdown_text += f" **{sub_key}:** {sub_value}\n" if "relevant file" in sub_key.lower():
if not gfm_supported: markdown_text += f"\n - **{sub_key}:** {sub_value} \n"
if "relevant line" not in sub_key.lower(): # nicer presentation else:
markdown_text += f" **{sub_key}:** {sub_value} \n"
if not gfm_supported:
if "relevant line" not in sub_key.lower(): # nicer presentation
# markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab # markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab
markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker
markdown_text += "\n" markdown_text += "\n"
return markdown_text return markdown_text
@ -282,66 +312,76 @@ def _fix_key_value(key: str, value: str):
try: try:
value = yaml.safe_load(value) value = yaml.safe_load(value)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to parse YAML for config override {key}={value}", exc_info=e) get_logger().debug(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
return key, value return key, value
def load_yaml(review_text: str) -> dict: def load_yaml(response_text: str) -> dict:
review_text = review_text.removeprefix('```yaml').rstrip('`') response_text = response_text.removeprefix('```yaml').rstrip('`')
try: try:
data = yaml.safe_load(review_text) data = yaml.safe_load(response_text)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to parse AI prediction: {e}") get_logger().error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text) data = try_fix_yaml(response_text)
return data return data
def try_fix_yaml(review_text: str) -> dict: def try_fix_yaml(response_text: str) -> dict:
review_text_lines = review_text.split('\n') response_text_lines = response_text.split('\n')
keys = ['relevant line:', 'suggestion content:', 'relevant file:']
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...' # first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
review_text_lines_copy = review_text_lines.copy() response_text_lines_copy = response_text_lines.copy()
for i in range(0, len(review_text_lines_copy)): for i in range(0, len(response_text_lines_copy)):
if 'relevant line:' in review_text_lines_copy[i] and not '|-' in review_text_lines_copy[i]: for key in keys:
review_text_lines_copy[i] = review_text_lines_copy[i].replace('relevant line: ', if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]:
'relevant line: |-\n ') response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}',
f'{key} |-\n ')
try: try:
data = yaml.load('\n'.join(review_text_lines_copy), Loader=yaml.SafeLoader) data = yaml.safe_load('\n'.join(response_text_lines_copy))
get_logger().info(f"Successfully parsed AI prediction after adding |-\n to relevant line") get_logger().info(f"Successfully parsed AI prediction after adding |-\n")
return data return data
except: except:
get_logger().debug(f"Failed to parse AI prediction after adding |-\n to relevant line") get_logger().info(f"Failed to parse AI prediction after adding |-\n")
# second fallback - try to remove last lines # second fallback - try to remove last lines
data = {} data = {}
for i in range(1, len(review_text_lines)): for i in range(1, len(response_text_lines)):
review_text_lines_tmp = '\n'.join(review_text_lines[:-i]) response_text_lines_tmp = '\n'.join(response_text_lines[:-i])
try: try:
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader) data = yaml.safe_load(response_text_lines_tmp,)
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines") get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
break break
except: except:
pass pass
return data
# thrid fallback - try to remove leading and trailing curly brackets
response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}')
try:
data = yaml.safe_load(response_text_copy,)
get_logger().info(f"Successfully parsed AI prediction after removing curly brackets")
return data
except:
pass
def set_custom_labels(variables): def set_custom_labels(variables, git_provider=None):
if not get_settings().config.enable_custom_labels: if not get_settings().config.enable_custom_labels:
return return
labels = get_settings().custom_labels labels = get_settings().custom_labels
if not labels: if not labels:
# set default labels # set default labels
labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other'] labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
labels_list = "\n - ".join(labels) if labels else "" labels_list = "\n - ".join(labels) if labels else ""
labels_list = f" - {labels_list}" if labels_list else "" labels_list = f" - {labels_list}" if labels_list else ""
variables["custom_labels"] = labels_list variables["custom_labels"] = labels_list
return return
final_labels = ""
for k, v in labels.items():
final_labels += f" - {k} ({v['description']})\n"
variables["custom_labels"] = final_labels
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
# Set custom labels
variables["custom_labels_class"] = "class Label(str, Enum):"
for k, v in labels.items():
description = v['description'].strip('\n').replace('\n', '\\n')
variables["custom_labels_class"] += f"\n {k.lower().replace(' ', '_')} = '{k}' # {description}"
def get_user_labels(current_labels: List[str] = None): def get_user_labels(current_labels: List[str] = None):
""" """
@ -352,7 +392,7 @@ def get_user_labels(current_labels: List[str] = None):
current_labels = [] current_labels = []
user_labels = [] user_labels = []
for label in current_labels: for label in current_labels:
if label.lower() in ['bug fix', 'tests', 'refactoring', 'enhancement', 'documentation', 'other']: if label.lower() in ['bug fix', 'tests', 'enhancement', 'documentation', 'other']:
continue continue
if get_settings().config.enable_custom_labels: if get_settings().config.enable_custom_labels:
if label in get_settings().custom_labels: if label in get_settings().custom_labels:
@ -368,8 +408,43 @@ def get_user_labels(current_labels: List[str] = None):
def get_max_tokens(model): def get_max_tokens(model):
settings = get_settings() settings = get_settings()
max_tokens_model = MAX_TOKENS[model] if model in MAX_TOKENS:
max_tokens_model = MAX_TOKENS[model]
else:
raise Exception(f"MAX_TOKENS must be set for model {model} in ./pr_agent/algo/__init__.py")
if settings.config.max_model_tokens: if settings.config.max_model_tokens:
max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model) max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model)
# get_logger().debug(f"limiting max tokens to {max_tokens_model}") # get_logger().debug(f"limiting max tokens to {max_tokens_model}")
return max_tokens_model return max_tokens_model
def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str:
"""
Clip the number of tokens in a string to a maximum number of tokens.
Args:
text (str): The string to clip.
max_tokens (int): The maximum number of tokens allowed in the string.
add_three_dots (bool, optional): A boolean indicating whether to add three dots at the end of the clipped
Returns:
str: The clipped string.
"""
if not text:
return 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]
if add_three_dots:
clipped_text += "...(truncated)"
return clipped_text
except Exception as e:
get_logger().warning(f"Failed to clip tokens: {e}")
return text

View File

@ -23,18 +23,22 @@ For example:
- cli.py --issue_url=... similar_issue - cli.py --issue_url=... similar_issue
Supported commands: Supported commands:
-review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement. - 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. - 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. - 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. - improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
Extended mode ('improve --extended') employs several calls, and provides a more thorough feedback Extended mode ('improve --extended') employs several calls, and provides a more thorough feedback
-reflect - Ask the PR author questions about the PR. - reflect - Ask the PR author questions about the PR.
-update_changelog - Update the changelog based on the PR's contents. - update_changelog - Update the changelog based on the PR's contents.
- add_docs
- generate_labels
Configuration: Configuration:

View File

@ -26,6 +26,7 @@ global_settings = Dynaconf(
"settings/pr_custom_labels.toml", "settings/pr_custom_labels.toml",
"settings/pr_add_docs.toml", "settings/pr_add_docs.toml",
"settings_prod/.secrets.toml", "settings_prod/.secrets.toml",
"settings_prod/.secrets_foo.toml",
"settings/custom_labels.toml" "settings/custom_labels.toml"
]] ]]
) )

View File

@ -1,5 +1,6 @@
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
from pr_agent.git_providers.github_provider import GithubProvider from pr_agent.git_providers.github_provider import GithubProvider
from pr_agent.git_providers.gitlab_provider import GitLabProvider from pr_agent.git_providers.gitlab_provider import GitLabProvider
@ -12,6 +13,7 @@ _GIT_PROVIDERS = {
'github': GithubProvider, 'github': GithubProvider,
'gitlab': GitLabProvider, 'gitlab': GitLabProvider,
'bitbucket': BitbucketProvider, 'bitbucket': BitbucketProvider,
'bitbucket_server': BitbucketServerProvider,
'azure': AzureDevopsProvider, 'azure': AzureDevopsProvider,
'codecommit': CodeCommitProvider, 'codecommit': CodeCommitProvider,
'local' : LocalGitProvider, 'local' : LocalGitProvider,

View File

@ -14,9 +14,8 @@ try:
except ImportError: except ImportError:
AZURE_DEVOPS_AVAILABLE = False AZURE_DEVOPS_AVAILABLE = False
from ..algo.pr_processing import clip_tokens
from ..config_loader import get_settings from ..config_loader import get_settings
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff, clip_tokens
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from .git_provider import EDIT_TYPE, FilePatchInfo from .git_provider import EDIT_TYPE, FilePatchInfo

View File

@ -228,6 +228,13 @@ class BitbucketProvider(GitProvider):
) )
return response return response
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
if relevant_line_start == -1:
link = f"{self.pr_url}/#L{relevant_file}"
else:
link = f"{self.pr_url}/#L{relevant_file}T{relevant_line_start}"
return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant file'].strip('`').strip("'")
@ -250,7 +257,15 @@ class BitbucketProvider(GitProvider):
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
for comment in comments: for comment in comments:
self.publish_inline_comment(comment['body'], comment['position'], comment['path']) if 'position' in comment:
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
elif 'start_line' in comment: # multi-line comment
# note that bitbucket does not seem to support range - only a comment on a single line - https://community.developer.atlassian.com/t/api-post-endpoint-for-inline-pull-request-comments/60452
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
elif 'line' in comment: # single-line comment
self.publish_inline_comment(comment['body'], comment['line'], comment['path'])
else:
get_logger().error(f"Could not publish inline comment {comment}")
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
@ -339,5 +354,5 @@ class BitbucketProvider(GitProvider):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_labels(self): def get_pr_labels(self):
pass pass

View File

@ -0,0 +1,351 @@
import json
from typing import Optional, Tuple
from urllib.parse import urlparse
import requests
from atlassian.bitbucket import Bitbucket
from starlette_context import context
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff
from ..config_loader import get_settings
from ..log import get_logger
class BitbucketServerProvider(GitProvider):
def __init__(
self, pr_url: Optional[str] = None, incremental: Optional[bool] = False
):
s = requests.Session()
try:
bearer = context.get("bitbucket_bearer_token", None)
s.headers["Authorization"] = f"Bearer {bearer}"
except Exception:
s.headers[
"Authorization"
] = f'Bearer {get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None)}'
s.headers["Content-Type"] = "application/json"
self.headers = s.headers
self.bitbucket_server_url = None
self.workspace_slug = None
self.repo_slug = None
self.repo = None
self.pr_num = None
self.pr = None
self.pr_url = pr_url
self.temp_comments = []
self.incremental = incremental
self.diff_files = None
self.bitbucket_pull_request_api_url = pr_url
self.bitbucket_server_url = self._parse_bitbucket_server(url=pr_url)
self.bitbucket_client = Bitbucket(url=self.bitbucket_server_url,
token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None))
if pr_url:
self.set_pr(pr_url)
def get_repo_settings(self):
try:
url = (f"{self.bitbucket_server_url}/projects/{self.workspace_slug}/repos/{self.repo_slug}/src/"
f"{self.pr.destination_branch}/.pr_agent.toml")
response = requests.request("GET", url, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text.encode('utf-8')
return contents
except Exception:
return ""
def publish_code_suggestions(self, code_suggestions: list) -> bool:
"""
Publishes code suggestions as comments on the PR.
"""
post_parameters_list = []
for suggestion in code_suggestions:
body = suggestion["body"]
relevant_file = suggestion["relevant_file"]
relevant_lines_start = suggestion["relevant_lines_start"]
relevant_lines_end = suggestion["relevant_lines_end"]
if not relevant_lines_start or relevant_lines_start == -1:
if get_settings().config.verbosity_level >= 2:
get_logger().exception(
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end < relevant_lines_start:
if get_settings().config.verbosity_level >= 2:
get_logger().exception(
f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end > relevant_lines_start:
post_parameters = {
"body": body,
"path": relevant_file,
"line": relevant_lines_end,
"start_line": relevant_lines_start,
"start_side": "RIGHT",
}
else: # API is different for single line comments
post_parameters = {
"body": body,
"path": relevant_file,
"line": relevant_lines_start,
"side": "RIGHT",
}
post_parameters_list.append(post_parameters)
try:
self.publish_inline_comments(post_parameters_list)
return True
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish code suggestion, error: {e}")
return False
def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'get_labels', 'gfm_markdown']:
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()
def get_file(self, path: str, commit_id: str):
file_content = ""
try:
file_content = self.bitbucket_client.get_content_of_file(self.workspace_slug,
self.repo_slug,
path,
commit_id)
except requests.HTTPError as e:
get_logger().debug(f"File {path} not found at commit id: {commit_id}")
return file_content
def get_files(self):
changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num)
diffstat = [change["path"]['toString'] for change in changes]
return diffstat
def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files:
return self.diff_files
commits_in_pr = self.bitbucket_client.get_pull_requests_commits(
self.workspace_slug,
self.repo_slug,
self.pr_num
)
commit_list = list(commits_in_pr)
base_sha, head_sha = commit_list[0]['parents'][0]['id'], commit_list[-1]['id']
diff_files = []
original_file_content_str = ""
new_file_content_str = ""
changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num)
for change in changes:
file_path = change['path']['toString']
match change['type']:
case 'ADD':
edit_type = EDIT_TYPE.ADDED
new_file_content_str = self.get_file(file_path, head_sha)
if isinstance(new_file_content_str, (bytes, bytearray)):
new_file_content_str = new_file_content_str.decode("utf-8")
original_file_content_str = ""
case 'DELETE':
edit_type = EDIT_TYPE.DELETED
new_file_content_str = ""
original_file_content_str = self.get_file(file_path, base_sha)
if isinstance(original_file_content_str, (bytes, bytearray)):
original_file_content_str = original_file_content_str.decode("utf-8")
case 'RENAME':
edit_type = EDIT_TYPE.RENAMED
case _:
edit_type = EDIT_TYPE.MODIFIED
original_file_content_str = self.get_file(file_path, base_sha)
if isinstance(original_file_content_str, (bytes, bytearray)):
original_file_content_str = original_file_content_str.decode("utf-8")
new_file_content_str = self.get_file(file_path, head_sha)
if isinstance(new_file_content_str, (bytes, bytearray)):
new_file_content_str = new_file_content_str.decode("utf-8")
patch = load_large_diff(file_path, new_file_content_str, original_file_content_str)
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
patch,
file_path,
edit_type=edit_type,
)
)
self.diff_files = diff_files
return diff_files
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if not is_temporary:
self.bitbucket_client.add_pull_request_comment(self.workspace_slug, self.repo_slug, self.pr_num, pr_comment)
def remove_initial_comment(self):
try:
for comment in self.temp_comments:
self.remove_comment(comment)
except ValueError as e:
get_logger().exception(f"Failed to remove temp comments, error: {e}")
def remove_comment(self, comment):
pass
# funtion to create_inline_comment
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
position, absolute_position = find_line_number_of_relevant_line_in_file(
self.get_diff_files(),
relevant_file.strip('`'),
relevant_line_in_file
)
if position == -1:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
subject_type = "FILE"
else:
subject_type = "LINE"
path = relevant_file.strip()
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
def publish_inline_comment(self, comment: str, from_line: int, file: str):
payload = {
"text": comment,
"severity": "NORMAL",
"anchor": {
"diffType": "EFFECTIVE",
"path": file,
"lineType": "ADDED",
"line": from_line,
"fileType": "TO"
}
}
response = requests.post(url=self._get_pr_comments_url(), json=payload, headers=self.headers)
return response
def generate_link_to_relevant_line_number(self, suggestion) -> str:
try:
relevant_file = suggestion['relevant file'].strip('`').strip("'")
relevant_line_str = suggestion['relevant line']
if not relevant_line_str:
return ""
diff_files = self.get_diff_files()
position, absolute_position = find_line_number_of_relevant_line_in_file \
(diff_files, relevant_file, relevant_line_str)
if absolute_position != -1 and self.pr_url:
link = f"{self.pr_url}/#L{relevant_file}T{absolute_position}"
return link
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Failed adding line link, error: {e}")
return ""
def publish_inline_comments(self, comments: list[dict]):
for comment in comments:
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
def get_title(self):
return self.pr.title
def get_languages(self):
return {"yaml": 0} # devops LOL
def get_pr_branch(self):
return self.pr.fromRef['displayId']
def get_pr_description_full(self):
return self.pr.description
def get_user_id(self):
return 0
def get_issue_comments(self):
raise NotImplementedError(
"Bitbucket provider does not support issue comments yet"
)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
return True
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
return True
@staticmethod
def _parse_bitbucket_server(url: str) -> str:
parsed_url = urlparse(url)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]:
parsed_url = urlparse(pr_url)
path_parts = parsed_url.path.strip("/").split("/")
if len(path_parts) < 6 or path_parts[4] != "pull-requests":
raise ValueError(
"The provided URL does not appear to be a Bitbucket PR URL"
)
workspace_slug = path_parts[1]
repo_slug = path_parts[3]
try:
pr_number = int(path_parts[5])
except ValueError as e:
raise ValueError("Unable to convert PR number to integer") from e
return workspace_slug, repo_slug, pr_number
def _get_repo(self):
if self.repo is None:
self.repo = self.bitbucket_client.get_repo(self.workspace_slug, self.repo_slug)
return self.repo
def _get_pr(self):
pr = self.bitbucket_client.get_pull_request(self.workspace_slug, self.repo_slug, pull_request_id=self.pr_num)
return type('new_dict', (object,), pr)
def _get_pr_file_content(self, remote_link: str):
return ""
def get_commit_messages(self):
def get_commit_messages(self):
raise NotImplementedError("Get commit messages function not implemented yet.")
# bitbucket does not support labels
def publish_description(self, pr_title: str, description: str):
payload = json.dumps({
"description": description,
"title": pr_title
})
response = requests.put(url=self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
return response
# bitbucket does not support labels
def publish_labels(self, pr_types: list):
pass
# bitbucket does not support labels
def get_pr_labels(self):
pass
def _get_pr_comments_url(self):
return f"{self.bitbucket_server_url}/rest/api/latest/projects/{self.workspace_slug}/repos/{self.repo_slug}/pull-requests/{self.pr_num}/comments"

View File

@ -6,9 +6,9 @@ from urllib.parse import urlparse
from pr_agent.git_providers.codecommit_client import CodeCommitClient from pr_agent.git_providers.codecommit_client import CodeCommitClient
from ..algo.language_handler import is_valid_file, language_extension_map
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
@ -61,6 +61,7 @@ class CodeCommitProvider(GitProvider):
self.pr = None self.pr = None
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.pr_url = pr_url
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
@ -215,7 +216,7 @@ class CodeCommitProvider(GitProvider):
def publish_labels(self, labels): def publish_labels(self, labels):
return [""] # not implemented yet return [""] # not implemented yet
def get_labels(self): def get_pr_labels(self):
return [""] # not implemented yet return [""] # not implemented yet
def remove_initial_comment(self): def remove_initial_comment(self):
@ -269,6 +270,8 @@ class CodeCommitProvider(GitProvider):
# where each dictionary item is a language name. # where each dictionary item is a language name.
# We build that language->extension dictionary here in main_extensions_flat. # We build that language->extension dictionary here in main_extensions_flat.
main_extensions_flat = {} main_extensions_flat = {}
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
for language, extensions in language_extension_map.items(): for language, extensions in language_extension_map.items():
for ext in extensions: for ext in extensions:
main_extensions_flat[ext] = language main_extensions_flat[ext] = language

View File

@ -192,7 +192,7 @@ class GerritProvider(GitProvider):
) )
self.repo = Repo(self.repo_path) self.repo = Repo(self.repo_path)
assert self.repo assert self.repo
self.pr_url = base_url
self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files()) self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files())
def get_pr_title(self): def get_pr_title(self):
@ -207,7 +207,7 @@ class GerritProvider(GitProvider):
Comment = namedtuple('Comment', ['body']) Comment = namedtuple('Comment', ['body'])
return Comments([Comment(c['message']) for c in reversed(comments)]) return Comments([Comment(c['message']) for c in reversed(comments)])
def get_labels(self): def get_pr_labels(self):
raise NotImplementedError( raise NotImplementedError(
'Getting labels is not implemented for the gerrit provider') 'Getting labels is not implemented for the gerrit provider')

View File

@ -5,6 +5,7 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -25,6 +26,8 @@ class FilePatchInfo:
tokens: int = -1 tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
old_filename: str = None old_filename: str = None
num_plus_lines: int = -1
num_minus_lines: int = -1
class GitProvider(ABC): class GitProvider(ABC):
@ -62,7 +65,7 @@ class GitProvider(ABC):
def get_pr_description(self, *, full: bool = True) -> str: def get_pr_description(self, *, full: bool = True) -> str:
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.algo.pr_processing import clip_tokens from pr_agent.algo.utils import clip_tokens
max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None) max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
description = self.get_pr_description_full() if full else self.get_user_description() description = self.get_pr_description_full() if full else self.get_user_description()
if max_tokens_description: if max_tokens_description:
@ -88,6 +91,9 @@ class GitProvider(ABC):
def get_pr_id(self): def get_pr_id(self):
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
return ""
#### comments operations #### #### comments operations ####
@abstractmethod @abstractmethod
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
@ -129,7 +135,10 @@ class GitProvider(ABC):
pass pass
@abstractmethod @abstractmethod
def get_labels(self): def get_pr_labels(self):
pass
def get_repo_labels(self):
pass pass
@abstractmethod @abstractmethod
@ -173,26 +182,42 @@ def get_main_pr_language(languages, files) -> str:
extension_list.append(file.filename.rsplit('.')[-1]) extension_list.append(file.filename.rsplit('.')[-1])
# get the most common extension # get the most common extension
most_common_extension = max(set(extension_list), key=extension_list.count) most_common_extension = '.' + max(set(extension_list), key=extension_list.count)
try:
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
# look for a match. TBD: add more languages, do this systematically if top_language in language_extension_map and most_common_extension in language_extension_map[top_language]:
if most_common_extension == 'py' and top_language == 'python' or \ main_language_str = top_language
most_common_extension == 'js' and top_language == 'javascript' or \ else:
most_common_extension == 'ts' and top_language == 'typescript' or \ for language, extensions in language_extension_map.items():
most_common_extension == 'go' and top_language == 'go' or \ if most_common_extension in extensions:
most_common_extension == 'java' and top_language == 'java' or \ main_language_str = language
most_common_extension == 'c' and top_language == 'c' or \ break
most_common_extension == 'cpp' and top_language == 'c++' or \ except Exception as e:
most_common_extension == 'cs' and top_language == 'c#' or \ get_logger().exception(f"Failed to get main language: {e}")
most_common_extension == 'swift' and top_language == 'swift' or \ pass
most_common_extension == 'php' and top_language == 'php' or \
most_common_extension == 'rb' and top_language == 'ruby' or \ ## old approach:
most_common_extension == 'rs' and top_language == 'rust' or \ # most_common_extension = max(set(extension_list), key=extension_list.count)
most_common_extension == 'scala' and top_language == 'scala' or \ # if most_common_extension == 'py' and top_language == 'python' or \
most_common_extension == 'kt' and top_language == 'kotlin' or \ # most_common_extension == 'js' and top_language == 'javascript' or \
most_common_extension == 'pl' and top_language == 'perl' or \ # most_common_extension == 'ts' and top_language == 'typescript' or \
most_common_extension == top_language: # most_common_extension == 'tsx' and top_language == 'typescript' or \
main_language_str = top_language # most_common_extension == 'go' and top_language == 'go' or \
# most_common_extension == 'java' and top_language == 'java' or \
# most_common_extension == 'c' and top_language == 'c' or \
# most_common_extension == 'cpp' and top_language == 'c++' or \
# most_common_extension == 'cs' and top_language == 'c#' or \
# most_common_extension == 'swift' and top_language == 'swift' or \
# most_common_extension == 'php' and top_language == 'php' or \
# most_common_extension == 'rb' and top_language == 'ruby' or \
# most_common_extension == 'rs' and top_language == 'rust' or \
# most_common_extension == 'scala' and top_language == 'scala' or \
# most_common_extension == 'kt' and top_language == 'kotlin' or \
# most_common_extension == 'pl' and top_language == 'perl' or \
# most_common_extension == top_language:
# main_language_str = top_language
except Exception as e: except Exception as e:
get_logger().exception(e) get_logger().exception(e)

View File

@ -8,8 +8,8 @@ from retry import retry
from starlette_context import context from starlette_context import context
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff, clip_tokens
from ..config_loader import get_settings from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
from ..servers.utils import RateLimitExceeded from ..servers.utils import RateLimitExceeded
@ -34,6 +34,7 @@ class GithubProvider(GitProvider):
if pr_url and 'pull' in pr_url: if pr_url and 'pull' in pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
self.last_commit_id = list(self.pr.get_commits())[-1] self.last_commit_id = list(self.pr.get_commits())[-1]
self.pr_url = self.get_pr_url() # pr_url for github actions can be as api.github.com, so we need to get the url from the pr object
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
return True return True
@ -60,6 +61,8 @@ class GithubProvider(GitProvider):
get_logger().info(f"Skipping merge commit {commit.commit.message}") get_logger().info(f"Skipping merge commit {commit.commit.message}")
continue continue
self.file_set.update({file.filename: file for file in commit.files}) self.file_set.update({file.filename: file for file in commit.files})
else:
raise ValueError("No previous review found")
def get_commit_range(self): def get_commit_range(self):
last_review_time = self.previous_review.created_at last_review_time = self.previous_review.created_at
@ -140,8 +143,15 @@ class GithubProvider(GitProvider):
else: else:
get_logger().error(f"Unknown edit type: {file.status}") get_logger().error(f"Unknown edit type: {file.status}")
edit_type = EDIT_TYPE.UNKNOWN edit_type = EDIT_TYPE.UNKNOWN
# count number of lines added and removed
patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch, file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch,
file.filename, edit_type=edit_type) file.filename, edit_type=edit_type,
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines,)
diff_files.append(file_patch_canonical_structure) diff_files.append(file_patch_canonical_structure)
self.diff_files = diff_files self.diff_files = diff_files
@ -405,7 +415,7 @@ class GithubProvider(GitProvider):
raise ValueError("GitHub app installation ID is required when using GitHub app deployment") raise ValueError("GitHub app installation ID is required when using GitHub app deployment")
auth = AppAuthentication(app_id=app_id, private_key=private_key, auth = AppAuthentication(app_id=app_id, private_key=private_key,
installation_id=self.installation_id) installation_id=self.installation_id)
return Github(app_auth=auth) return Github(app_auth=auth, base_url=get_settings().github.base_url)
if deployment_type == 'user': if deployment_type == 'user':
try: try:
@ -414,7 +424,7 @@ class GithubProvider(GitProvider):
raise ValueError( raise ValueError(
"GitHub token is required when using user deployment. See: " "GitHub token is required when using user deployment. See: "
"https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e "https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e
return Github(auth=Auth.Token(token)) return Github(auth=Auth.Token(token), base_url=get_settings().github.base_url)
def _get_repo(self): def _get_repo(self):
if hasattr(self, 'repo_obj') and \ if hasattr(self, 'repo_obj') and \
@ -439,7 +449,7 @@ class GithubProvider(GitProvider):
def publish_labels(self, pr_types): def publish_labels(self, pr_types):
try: try:
label_color_map = {"Bug fix": "1d76db", "Tests": "e99695", "Bug fix with tests": "c5def5", label_color_map = {"Bug fix": "1d76db", "Tests": "e99695", "Bug fix with tests": "c5def5",
"Refactoring": "bfdadc", "Enhancement": "bfd4f2", "Documentation": "d4c5f9", "Enhancement": "bfd4f2", "Documentation": "d4c5f9",
"Other": "d1bcf9"} "Other": "d1bcf9"}
post_parameters = [] post_parameters = []
for p in pr_types: for p in pr_types:
@ -451,13 +461,17 @@ class GithubProvider(GitProvider):
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().exception(f"Failed to publish labels, error: {e}")
def get_labels(self): def get_pr_labels(self):
try: try:
return [label.name for label in self.pr.labels] return [label.name for label in self.pr.labels]
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to get labels, error: {e}") get_logger().exception(f"Failed to get labels, error: {e}")
return [] return []
def get_repo_labels(self):
labels = self.repo_obj.get_labels()
return [label for label in labels]
def get_commit_messages(self): def get_commit_messages(self):
""" """
Retrieves the commit messages of a pull request. Retrieves the commit messages of a pull request.
@ -501,6 +515,17 @@ class GithubProvider(GitProvider):
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
sha_file = hashlib.sha256(relevant_file.encode('utf-8')).hexdigest()
if relevant_line_start == -1:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}"
elif relevant_line_end:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}-R{relevant_line_end}"
else:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}"
return link
def get_pr_id(self): def get_pr_id(self):
try: try:
pr_id = f"{self.repo}/{self.pr_num}" pr_id = f"{self.repo}/{self.pr_num}"

View File

@ -7,8 +7,8 @@ import gitlab
from gitlab import GitlabGetError from gitlab import GitlabGetError
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff, clip_tokens
from ..config_loader import get_settings from ..config_loader import get_settings
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..log import get_logger from ..log import get_logger
@ -37,13 +37,14 @@ class GitLabProvider(GitProvider):
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.temp_comments = [] self.temp_comments = []
self.pr_url = merge_request_url
self._set_merge_request(merge_request_url) self._set_merge_request(merge_request_url)
self.RE_HUNK_HEADER = re.compile( self.RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
self.incremental = incremental self.incremental = incremental
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', 'gfm_markdown']: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']: # gfm_markdown is supported in gitlab !
return False return False
return True return True
@ -114,12 +115,20 @@ class GitLabProvider(GitProvider):
if not patch: if not patch:
patch = load_large_diff(filename, new_file_content_str, original_file_content_str) patch = load_large_diff(filename, new_file_content_str, original_file_content_str)
# count number of lines added and removed
patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
diff_files.append( diff_files.append(
FilePatchInfo(original_file_content_str, new_file_content_str, FilePatchInfo(original_file_content_str, new_file_content_str,
patch=patch, patch=patch,
filename=filename, filename=filename,
edit_type=edit_type, edit_type=edit_type,
old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path'])) old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path'],
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines, ))
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
@ -202,7 +211,11 @@ class GitLabProvider(GitProvider):
pos_obj['new_line'] = target_line_no - 1 pos_obj['new_line'] = target_line_no - 1
pos_obj['old_line'] = source_line_no - 1 pos_obj['old_line'] = source_line_no - 1
get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}")
self.mr.discussions.create({'body': body, 'position': pos_obj}) try:
self.mr.discussions.create({'body': body, 'position': pos_obj})
except Exception as e:
get_logger().debug(
f"Failed to create comment in {self.id_mr} with position {pos_obj} (probably not a '+' line)")
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: 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 changes = self.mr.changes() # Retrieve the changes for the merge request once
@ -395,7 +408,7 @@ class GitLabProvider(GitProvider):
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
pass pass
def get_labels(self): def get_pr_labels(self):
return self.mr.labels return self.mr.labels
def get_commit_messages(self): def get_commit_messages(self):
@ -422,6 +435,16 @@ class GitLabProvider(GitProvider):
except: except:
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
if relevant_line_start == -1:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads"
elif relevant_line_end:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}-L{relevant_line_end}"
else:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}"
return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant file'].strip('`').strip("'")

View File

@ -178,5 +178,5 @@ class LocalGitProvider(GitProvider):
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError('Getting issue comments is not implemented for the local git provider') raise NotImplementedError('Getting issue comments is not implemented for the local git provider')
def get_labels(self): def get_pr_labels(self):
raise NotImplementedError('Getting labels is not implemented for the local git provider') raise NotImplementedError('Getting labels is not implemented for the local git provider')

View File

@ -16,8 +16,13 @@ from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings, global_settings from pr_agent.config_loader import get_settings, global_settings
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider from pr_agent.secret_providers import get_secret_provider
from pr_agent.servers.github_action_runner import get_setting_or_env, is_true
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer
setup_logger(fmt=LoggingFormat.JSON) setup_logger(fmt=LoggingFormat.JSON)
router = APIRouter() router = APIRouter()
@ -89,8 +94,20 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "pull_request" log_context["event"] = "pull_request"
with get_logger().contextualize(**log_context): if pr_url:
await agent.handle_request(pr_url, "review") with get_logger().contextualize(**log_context):
apply_repo_settings(pr_url)
auto_review = get_setting_or_env("BITBUCKET_APP.AUTO_REVIEW", None)
if auto_review is None or is_true(auto_review): # by default, auto review is enabled
await PRReviewer(pr_url).run()
auto_improve = get_setting_or_env("BITBUCKET_APP.AUTO_IMPROVE", None)
if is_true(auto_improve): # by default, auto improve is disabled
await PRCodeSuggestions(pr_url).run()
auto_describe = get_setting_or_env("BITBUCKET_APP.AUTO_DESCRIBE", None)
if is_true(auto_describe): # by default, auto describe is disabled
await PRDescription(pr_url).run()
# with get_logger().contextualize(**log_context):
# await agent.handle_request(pr_url, "review")
elif event == "pullrequest:comment_created": elif event == "pullrequest:comment_created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url

View File

@ -0,0 +1,64 @@
import json
import uvicorn
from fastapi import APIRouter, FastAPI
from fastapi.encoders import jsonable_encoder
from starlette import status
from starlette.background import BackgroundTasks
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
router = APIRouter()
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict):
log_context["action"] = body
log_context["event"] = "pull_request" if body == "review" else "comment"
log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body)
@router.post("/webhook")
async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "bitbucket_server"}
data = await request.json()
get_logger().info(json.dumps(data))
pr_id = data['pullRequest']['id']
repository_name = data['pullRequest']['toRef']['repository']['slug']
project_name = data['pullRequest']['toRef']['repository']['project']['key']
bitbucket_server = get_settings().get("BITBUCKET_SERVER.URL")
pr_url = f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}"
log_context["api_url"] = pr_url
log_context["event"] = "pull_request"
handle_request(background_tasks, pr_url, "review", log_context)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
@router.get("/")
async def root():
return {"status": "ok"}
def start():
bitbucket_server_url = get_settings().get("BITBUCKET_SERVER.URL", None)
if not bitbucket_server_url:
raise ValueError("BITBUCKET_SERVER.URL is not set")
get_settings().config.git_provider = "bitbucket_server"
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=3000)
if __name__ == '__main__':
start()

View File

@ -1,6 +1,7 @@
import asyncio import asyncio
import json import json
import os import os
from typing import Union
from pr_agent.agent.pr_agent import PRAgent from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
@ -12,6 +13,22 @@ from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
def is_true(value: Union[str, bool]) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() == 'true'
return False
def get_setting_or_env(key: str, default: Union[str, bool] = None) -> Union[str, bool]:
try:
value = get_settings().get(key, default)
except AttributeError: # TBD still need to debug why this happens on GitHub Actions
value = os.getenv(key, None) or os.getenv(key.upper(), None) or os.getenv(key.lower(), None) or default
return value
async def run_action(): async def run_action():
# Get environment variables # Get environment variables
GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME') GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME')
@ -65,14 +82,14 @@ async def run_action():
if action in ["opened", "reopened"]: if action in ["opened", "reopened"]:
pr_url = event_payload.get("pull_request", {}).get("url") pr_url = event_payload.get("pull_request", {}).get("url")
if pr_url: if pr_url:
auto_review = os.environ.get('github_action.auto_review', None) auto_review = get_setting_or_env("GITHUB_ACTION.AUTO_REVIEW", None)
if auto_review is None or (isinstance(auto_review, str) and auto_review.lower() == 'true'): if auto_review is None or is_true(auto_review):
await PRReviewer(pr_url).run() await PRReviewer(pr_url).run()
auto_describe = os.environ.get('github_action.auto_describe', None) auto_describe = get_setting_or_env("GITHUB_ACTION.AUTO_DESCRIBE", None)
if isinstance(auto_describe, str) and auto_describe.lower() == 'true': if is_true(auto_describe):
await PRDescription(pr_url).run() await PRDescription(pr_url).run()
auto_improve = os.environ.get('github_action.auto_improve', None) auto_improve = get_setting_or_env("GITHUB_ACTION.AUTO_IMPROVE", None)
if isinstance(auto_improve, str) and auto_improve.lower() == 'true': if is_true(auto_improve):
await PRCodeSuggestions(pr_url).run() await PRCodeSuggestions(pr_url).run()
# Handle issue comment event # Handle issue comment event

View File

@ -125,11 +125,15 @@ async def handle_request(body: Dict[str, Any], event: str):
await _perform_commands("pr_commands", agent, body, api_url, log_context) await _perform_commands("pr_commands", agent, body, api_url, log_context)
# handle pull_request event with synchronize action - "push trigger" for new commits # handle pull_request event with synchronize action - "push trigger" for new commits
elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger: elif event == 'pull_request' and action == 'synchronize':
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user) pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
if not (pull_request and api_url): if not (pull_request and api_url):
return {} return {}
apply_repo_settings(api_url)
if not get_settings().github_app.handle_push_trigger:
return {}
# TODO: do we still want to get the list of commits to filter bot/merge commits? # TODO: do we still want to get the list of commits to filter bot/merge commits?
before_sha = body.get("before") before_sha = body.get("before")
after_sha = body.get("after") after_sha = body.get("after")

View File

@ -38,7 +38,7 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
try: try:
secret_dict = json.loads(secret) secret_dict = json.loads(secret)
gitlab_token = secret_dict["gitlab_token"] gitlab_token = secret_dict["gitlab_token"]
log_context["sender"] = secret_dict["id"] log_context["sender"] = secret_dict.get("token_name", secret_dict.get("id", "unknown"))
context["settings"] = copy.deepcopy(global_settings) context["settings"] = copy.deepcopy(global_settings)
context["settings"].gitlab.personal_access_token = gitlab_token context["settings"].gitlab.personal_access_token = gitlab_token
except Exception as e: except Exception as e:

View File

@ -1,17 +1,19 @@
commands_text = "> **/review [-i]**: Request a review of your Pull Request. For an incremental review, which only " \ commands_text = "> **/review**: Request a review of your Pull Request. \n" \
"considers changes since the last review, include the '-i' option.\n" \ "> **/describe**: Update 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 [--extended]**: Suggest code improvements. Extended mode provides a higher quality feedback. \n" \
"> **/improve [--extended]**: Suggest improvements to the code in the PR. Extended mode employs several calls, and provides a more thorough feedback. \n" \ "> **/ask \\<QUESTION\\>**: Ask a question about the PR. \n" \
"> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n" \ "> **/update_changelog**: Update the changelog based on the PR's contents. \n" \
"> **/update_changelog**: Update the changelog based on the PR's contents.\n\n" \ "> **/add_docs**: Generate docstring for new components introduced in the PR. \n" \
">To edit any configuration parameter from **configuration.toml**, add --config_path=new_value\n" \ "> **/generate_labels**: Generate labels for the PR based on the PR's contents. \n" \
">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \ "> see the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n\n" \
">To list the possible configuration parameters, use the **/config** command.\n" \ ">To edit any configuration parameter from the [configuration.toml](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/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, add a **/config** comment. \n" \
def bot_help_text(user: str): def bot_help_text(user: str):
return f"> Tag me in a comment '@{user}' and add one of the following commands:\n" + commands_text return f"> Tag me in a comment '@{user}' and add one of the following commands: \n" + commands_text
actions_help_text = "> To invoke the PR-Agent, add a comment using one of the following commands:\n" + \ actions_help_text = "> To invoke the PR-Agent, add a comment using one of the following commands: \n" + \
commands_text commands_text

View File

@ -3,10 +3,8 @@ from mangum import Mangum
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette_context.middleware import RawContextMiddleware from starlette_context.middleware import RawContextMiddleware
from pr_agent.log import setup_logger
from pr_agent.servers.github_app import router from pr_agent.servers.github_app import router
setup_logger()
middleware = [Middleware(RawContextMiddleware)] middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware) app = FastAPI(middleware=middleware)

View File

@ -40,6 +40,9 @@ api_base = "" # the base url for your local Llama 2, Code Llama, and other model
vertex_project = "" # the google cloud platform project name for your vertexai deployment vertex_project = "" # the google cloud platform project name for your vertexai deployment
vertex_location = "" # the google cloud platform location for your vertexai deployment vertex_location = "" # the google cloud platform location for your vertexai deployment
[aws]
bedrock_region = "" # the AWS region to call Bedrock APIs
[github] [github]
# ---- Set the following only for deployment type == "user" # ---- Set the following only for deployment type == "user"
user_token = "" # A GitHub personal access token with 'repo' scope. user_token = "" # A GitHub personal access token with 'repo' scope.

View File

@ -46,17 +46,22 @@ keep_original_user_title=false
use_bullet_points=true use_bullet_points=true
extra_instructions = "" extra_instructions = ""
enable_pr_type=true enable_pr_type=true
enable_file_walkthrough=false
enable_semantic_files_types=true
final_update_message = true
# markers # markers
use_description_markers=false use_description_markers=false
include_generated_by_header=true include_generated_by_header=true
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other'] #custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
[pr_questions] # /ask # [pr_questions] # /ask #
[pr_code_suggestions] # /improve # [pr_code_suggestions] # /improve #
num_code_suggestions=4 num_code_suggestions=4
summarize = false
extra_instructions = "" extra_instructions = ""
rank_suggestions = false rank_suggestions = false
# params for '/improve --extended' mode # params for '/improve --extended' mode
@ -79,6 +84,7 @@ extra_instructions = ""
# The type of deployment to create. Valid values are 'app' or 'user'. # The type of deployment to create. Valid values are 'app' or 'user'.
deployment_type = "user" deployment_type = "user"
ratelimit_retries = 5 ratelimit_retries = 5
base_url = "https://api.github.com"
[github_action] [github_action]
# auto_review = true # set as env var in .github/workflows/pr-agent.yaml # auto_review = true # set as env var in .github/workflows/pr-agent.yaml
@ -137,6 +143,12 @@ magic_word = "AutoReview"
# Polling interval # Polling interval
polling_interval_seconds = 30 polling_interval_seconds = 30
[bitbucket_app]
#auto_review = true # set as config var in .pr_agent.toml
#auto_describe = true # set as config var in .pr_agent.toml
#auto_improve = true # set as config var in .pr_agent.toml
[local] [local]
# LocalGitProvider settings - uncomment to use paths other than default # LocalGitProvider settings - uncomment to use paths other than default
# description_path= "path/to/description.md" # description_path= "path/to/description.md"
@ -164,3 +176,4 @@ max_issues_to_scan = 500
# fill and place in .secrets.toml # fill and place in .secrets.toml
#api_key = ... #api_key = ...
# environment = "gcp-starter" # environment = "gcp-starter"

View File

@ -3,16 +3,14 @@ enable_custom_labels=false
## template for custom labels ## template for custom labels
#[custom_labels."Bug fix"] #[custom_labels."Bug fix"]
#description = "Fixes a bug in the code" #description = """Fixes a bug in the code"""
#[custom_labels."Tests"] #[custom_labels."Tests"]
#description = "Adds or modifies tests" #description = """Adds or modifies tests"""
#[custom_labels."Bug fix with tests"] #[custom_labels."Bug fix with tests"]
#description = "Fixes a bug in the code and adds or modifies tests" #description = """Fixes a bug in the code and adds or modifies tests"""
#[custom_labels."Refactoring"]
#description = "Code refactoring without changing functionality"
#[custom_labels."Enhancement"] #[custom_labels."Enhancement"]
#description = "Adds new features or functionality" #description = """Adds new features or modifies existing ones"""
#[custom_labels."Documentation"] #[custom_labels."Documentation"]
#description = "Adds or modifies documentation" #description = """Adds or modifies documentation"""
#[custom_labels."Other"] #[custom_labels."Other"]
#description = "Other changes that do not fit in any of the above categories" #description = """Other changes that do not fit in any of the above categories"""

View File

@ -1,22 +1,22 @@
[pr_add_docs_prompt] [pr_add_docs_prompt]
system="""You are a language model called PR-Code-Documentation Agent, that specializes in generating documentation for code. system="""You are PR-Doc, a language model that specializes in generating documentation for code components in a Pull Request (PR).
Your task is to generate meaningfull {{ docs_for_language }} to a PR (the '+' lines). Your task is to generate {{ docs_for_language }} for code components in the PR Diff.
Example for a PR Diff input:
' Example for the PR Diff format:
======
## src/file1.py ## src/file1.py
@@ -12,3 +12,5 @@ def func1(): @@ -12,3 +12,4 @@ def func1():
__new hunk__ __new hunk__
12 code line that already existed in the file... 12 code line1 that remained unchanged in the PR
13 code line that already existed in the file....
14 +new code line1 added in the PR 14 +new code line1 added in the PR
15 +new code line2 added in the PR 15 +new code line2 added in the PR
16 code line that already existed in the file... 16 code line2 that remained unchanged in the PR
__old hunk__ __old hunk__
code line that already existed in the file... code line1 that remained unchanged in the PR
-code line that was removed in the PR -code line that was removed in the PR
code line that already existed in the file... code line2 that remained unchanged in the PR
@@ ... @@ def func2(): @@ ... @@ def func2():
@ -28,12 +28,13 @@ __old hunk__
## src/file2.py ## src/file2.py
... ...
' ======
Specific instructions: Specific instructions:
- Try to identify edited/added code components (classes/functions/methods...) that are undocumented. and generate {{ docs_for_language }} for each one. - Try to identify edited/added code components (classes/functions/methods...) that are undocumented, and generate {{ docs_for_language }} for each one.
- If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them. - If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them.
- Ignore code components that don't appear fully in the '__new hunk__' section. For example. you must see the component header and body, - Ignore code components that don't appear fully in the '__new hunk__' section. For example, you must see the component header and body.
- Make sure the {{ docs_for_language }} starts and ends with standart {{ language }} {{ docs_for_language }} signs. - Make sure the {{ docs_for_language }} starts and ends with standart {{ language }} {{ docs_for_language }} signs.
- The {{ docs_for_language }} should be in standard format. - The {{ docs_for_language }} should be in standard format.
- Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added. - Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added.
@ -42,11 +43,12 @@ Specific instructions:
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' ======
{{ extra_instructions }} {{ extra_instructions }}
' ======
{%- endif %} {%- endif %}
You must use the following YAML schema to format your answer: You must use the following YAML schema to format your answer:
```yaml ```yaml
Code Documentation: Code Documentation:
@ -99,18 +101,25 @@ Title: '{{ title }}'
Branch: '{{ branch }}' Branch: '{{ branch }}'
Description: '{{description}}' {%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}} Main PR language: '{{language}}'
{%- endif %} {%- endif %}
The PR Diff: The PR Diff:
``` ======
{{- diff|trim }} {{ diff|trim }}
``` ======
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -1,22 +1,21 @@
[pr_code_suggestions_prompt] [pr_code_suggestions_prompt]
system="""You are a language model called PR-Code-Reviewer, that specializes in suggesting code improvements for Pull Request (PR). system="""You are PR-Reviewer, a language model that specializes in suggesting code improvements for a Pull Request (PR).
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR (the '+' lines in the diff). Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR diff (lines starting with '+').
Example for a PR Diff input: Example for the PR Diff format:
' ======
## src/file1.py ## src/file1.py
@@ -12,3 +12,5 @@ def func1(): @@ -12,3 +12,4 @@ def func1():
__new hunk__ __new hunk__
12 code line that already existed in the file... 12 code line1 that remained unchanged in the PR
13 code line that already existed in the file....
14 +new code line1 added in the PR 14 +new code line1 added in the PR
15 +new code line2 added in the PR 15 +new code line2 added in the PR
16 code line that already existed in the file... 16 code line2 that remained unchanged in the PR
__old hunk__ __old hunk__
code line that already existed in the file... code line1 that remained unchanged in the PR
-code line that was removed in the PR -code line that was removed in the PR
code line that already existed in the file... code line2 that remained unchanged in the PR
@@ ... @@ def func2(): @@ ... @@ def func2():
@ -28,28 +27,29 @@ __old hunk__
## src/file2.py ## src/file2.py
... ...
' ======
Specific instructions: Specific instructions:
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. - Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
- Prioritize suggestions that address major problems, issues and bugs in the code. - Prioritize suggestions that address major problems, issues and bugs in the code. As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects.
As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects.
- Don't suggest to add docstring, type hints, or comments. - Don't suggest to add docstring, type hints, or comments.
- Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+'). - Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+').
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code. - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code.
- For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code. - For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code.
- Provide the exact line numbers range (inclusive) for each issue. - Provide the exact line numbers range (inclusive) for each suggestion.
- Assume there is additional relevant code, that is not included in the diff. - Assume there is additional relevant code, that is not included in the diff.
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' ======
{{ extra_instructions }} {{ extra_instructions }}
' ======
{%- endif %} {%- endif %}
You must use the following YAML schema to format your answer: You must use the following YAML schema to format your answer:
```yaml ```yaml
Code suggestions: Code suggestions:
@ -90,16 +90,19 @@ Code suggestions:
Example output: Example output:
```yaml ```yaml
Code suggestions: Code suggestions:
- relevant file: |- - relevant file: |-
src/file1.py src/file1.py
suggestion content: |- suggestion content: |-
Add a docstring to func1() Add a docstring to func1()
existing code: |- existing code: |-
def func1(): def func1():
relevant lines start: 12 relevant lines start: |-
relevant lines end: 12 12
improved code: |- relevant lines end: |-
... 12
improved code: |-
...
...
``` ```
@ -113,18 +116,25 @@ Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}' {%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}} Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
The PR Diff: The PR Diff:
``` ======
{{- diff|trim }} {{ diff|trim }}
``` ======
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -1,71 +1,86 @@
[pr_custom_labels_prompt] [pr_custom_labels_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to label the type of the PR content. Your task is to provide labels that describe the PR content.
- Make sure not to focus the new PR code (the '+' lines). {%- if enable_custom_labels %}
- If needed, each YAML output should be in block scalar format ('|-') Thoroughly read the labels name and the provided description, and decide whether the label is relevant to the PR.
{%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' ======
{{ extra_instructions }} {{ extra_instructions }}
' ======
{% endif %} {% endif %}
You must use the following YAML schema to format your answer:
```yaml The output must be a YAML object equivalent to type $Labels, according to the following Pydantic definitions:
PR Type: ======
type: array
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
description: Labels that are applicable to the Pull Request. Don't output the description in the parentheses. If none of the labels is relevant to the PR, output an empty array.
{%- endif %} {{ custom_labels_class }}
items:
type: string
enum:
{%- if enable_custom_labels %}
{{ custom_labels }}
{%- else %} {%- else %}
- Bug fix class Label(str, Enum):
- Tests bug_fix = "Bug fix"
- Refactoring tests = "Tests"
- Enhancement enhancement = "Enhancement"
- Documentation documentation = "Documentation"
- Other other = "Other"
{%- endif %} {%- endif %}
class Labels(BaseModel):
labels: List[Label] = Field(min_items=0, description="custom labels that describe the PR. Return the label value, not the name.")
======
Example output: Example output:
```yaml ```yaml
PR Type: labels:
{%- if enable_custom_labels %} - ...
{{ custom_labels_examples }} - ...
{%- else %}
- Bug fix
{%- endif %}
``` ```
Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields. Answer should be a valid YAML, and nothing else.
""" """
user="""PR Info: user="""PR Info:
Previous title: '{{title}}' Previous title: '{{title}}'
Previous description: '{{description}}'
Branch: '{{branch}}' Branch: '{{ branch }}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}} Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
{{commit_messages_str}} ======
{{ commit_messages_str|trim }}
======
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
``` ======
{{diff}} {{ diff|trim }}
``` ======
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. 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 YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml
""" """

View File

@ -1,102 +1,133 @@
[pr_description_prompt] [pr_description_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to provide full description of a Pull Request (PR) content. Your task is to provide a full description for the PR content - title, type, description, and main files walkthrough.
- Make sure to focus on the new PR code (the '+' lines). - Focus on the new PR code (lines starting with '+').
- 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. - Keep in mind that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or out of date. Hence, compare them to the PR diff code, and use them only as a reference.
- Emphasize first the most important changes, and then the less important ones. - The generated title and description should prioritize the most significant changes.
- If needed, each YAML output should be in block scalar format ('|-') - If needed, each YAML output should be in block scalar indicator ('|-')
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' =====
{{ extra_instructions }} {{ extra_instructions }}
' =====
{% endif %} {% endif %}
You must use the following YAML schema to format your answer:
```yaml The output must be a YAML object equivalent to type $PRDescription, according to the following Pydantic definitions:
PR Title: =====
type: string class PRType(str, Enum):
description: an informative title for the PR, describing its main theme bug_fix = "Bug fix"
PR Type: tests = "Tests"
type: string enhancement = "Enhancement"
enum: documentation = "Documentation"
- Bug fix other = "Other"
- Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
PR Labels:
type: array {{ custom_labels_class }}
description: Labels that are applicable to the Pull Request. Don't output the description in the parentheses. If none of the labels is relevant to the PR, output an empty array.
items:
type: string
enum:
{{ custom_labels }}
{%- endif %} {%- endif %}
PR Description:
type: string {%- if enable_file_walkthrough %}
description: an informative and concise description of the PR. class FileWalkthrough(BaseModel):
{%- if use_bullet_points %} Use bullet points. {% endif %} filename: str = Field(description="the relevant file full path")
PR Main Files Walkthrough: changes_in_file: str = Field(description="minimal and concise summary of the changes in the relevant file")
type: array {%- endif %}
maxItems: 10
description: |- {%- if enable_semantic_files_types %}
a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files). Class FileDescription(BaseModel):
items: filename: str = Field(description="the relevant file full path")
filename: changes_summary: str = Field(description="minimal and concise summary of the changes in the relevant file")
type: string label: str = Field(description="a single semantic label that represents a type of code changes that occurred in the File. Possible values (partial list): 'bug fix', 'tests', 'enhancement', 'documentation', 'error handling', 'configuration changes', 'dependencies', 'formatting', 'miscellaneous', ...")
description: the relevant file full path {%- endif %}
changes in file:
type: string Class PRDescription(BaseModel):
description: minimal and concise description of the changes in the relevant file title: str = Field(description="an informative title for the PR, describing its main theme")
``` type: List[PRType] = Field(description="one or more types that describe the PR type. Return the label value, not the name.")
description: str = Field(description="an informative and concise description of the PR. {%- if use_bullet_points %} Use bullet points.{% endif %}")
{%- if enable_custom_labels %}
labels: List[Label] = Field(min_items=0, description="custom labels that describe the PR. Return the label value, not the name.")
{%- endif %}
{%- if enable_file_walkthrough %}
main_files_walkthrough: List[FileWalkthrough] = Field(max_items=10)
{%- endif %}
{%- if enable_semantic_files_types %}
pr_files[List[FileDescription]] = Field(max_items=15")
{%- endif %}
=====
Example output: Example output:
```yaml ```yaml
PR Title: |- title: |-
...
PR Type:
... ...
type:
- ...
- ...
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
PR Labels: labels:
- ... - ...
- ... - ...
{%- endif %} {%- endif %}
PR Description: |- description: |-
... ...
PR Main Files Walkthrough: {%- if enable_file_walkthrough %}
- ... main_files_walkthrough:
- ... - ...
- ...
{%- endif %}
{%- if enable_semantic_files_types %}
pr_files:
- filename: |
...
changes_summary: |
...
label: |
...
...
{%- endif %}
``` ```
Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields. Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|-')
""" """
user="""PR Info: user="""PR Info:
Previous title: '{{title}}' Previous title: '{{title}}'
Previous description: '{{description}}'
{%- if description %}
Previous description:
=====
{{ description|trim }}
=====
{%- endif %}
Branch: '{{branch}}' Branch: '{{branch}}'
{%- if language %} {%- if language %}
Main language: {{language}} Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
{{commit_messages_str}} =====
{{ commit_messages_str|trim }}
=====
{%- endif %} {%- endif %}
The PR Git Diff: The PR Diff:
``` =====
{{diff}} {{ diff|trim }}
``` =====
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. 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 YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml
""" """

View File

@ -1,5 +1,5 @@
[pr_information_from_user_prompt] [pr_information_from_user_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code 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. 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.
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. 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.
@ -16,22 +16,36 @@ Questions to better understand the PR:
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
{{commit_messages_str}} ======
{{ commit_messages_str|trim }}
======
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
``` ======
{{diff}} {{ diff|trim }}
``` ======
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 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

View File

@ -1,36 +1,42 @@
[pr_questions_prompt] [pr_questions_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to answer questions about the new PR code (the '+' lines), and provide feedback.
Your goal is to answer questions\\tasks about the new PR code (lines starting with '+'), and provide feedback.
Be informative, constructive, and give examples. Try to be as specific as possible. Be informative, constructive, and give examples. Try to be as specific as possible.
Don't avoid answering the questions. You must answer the questions, as best as you can, without adding unrelated content. Don't avoid answering the questions. You must answer the questions, as best as you can, without adding any unrelated content.
Make sure not to repeat modifications already implemented in the new PR code (the '+' lines).
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}'
Branch: '{{branch}}'
Description: '{{description}}'
{%- if language %}
Main language: {{language}}
{%- endif %}
{%- if commit_messages_str %}
Commit messages: Title: '{{title}}'
{{commit_messages_str}}
Branch: '{{branch}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
``` ======
{{diff}} {{ diff|trim }}
``` ======
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 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
The PR Questions: The PR Questions:
``` ======
{{ questions }} {{ questions|trim }}
``` ======
Response: Response to the PR Questions:
""" """

View File

@ -1,18 +1,19 @@
[pr_review_prompt] [pr_review_prompt]
system="""You are PR-Reviewer, a language model designed to review git pull requests. system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to provide constructive and concise feedback for the PR, and also provide meaningful code suggestions. Your task is to provide constructive and concise feedback for the PR, and also provide meaningful code suggestions.
The review should focus on new code added in the PR diff (lines starting with '+')
Example PR Diff input: Example PR Diff:
' ======
## src/file1.py ## src/file1.py
@@ -12,5 +12,5 @@ def func1(): @@ -12,5 +12,5 @@ def func1():
code line that already existed in the file... code line 1 that remained unchanged in the PR
code line that already existed in the file.... code line 2 that remained unchanged in the PR
-code line that was removed in the PR -code line that was removed in the PR
+new code line added in the PR +code line added in the PR
code line that already existed in the file... code line 3 that remained unchanged in the PR
code line that already existed in the file...
@@ ... @@ def func2(): @@ ... @@ def func2():
... ...
@ -20,26 +21,28 @@ code line that already existed in the file....
## src/file2.py ## src/file2.py
... ...
' ======
The review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
{%- if num_code_suggestions > 0 %} {%- if num_code_suggestions > 0 %}
Code suggestions guidelines:
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. - Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices. - Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code. - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
- Don't suggest to add docstring, type hints, or comments. - Don't suggest to add docstring, type hints, or comments.
- Suggestions should focus on improving the new code added in the PR (lines starting with '+') - Suggestions should focus on the new code added in the PR diff (lines starting with '+')
{%- endif %} {%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' ======
{{ extra_instructions }} {{ extra_instructions }}
' ======
{% endif %} {% endif %}
You must use the following YAML schema to format your answer: You must use the following YAML schema to format your answer:
```yaml ```yaml
PR Analysis: PR Analysis:
@ -54,7 +57,6 @@ PR Analysis:
enum: enum:
- Bug fix - Bug fix
- Tests - Tests
- Refactoring
- Enhancement - Enhancement
- Documentation - Documentation
- Other - Other
@ -179,36 +181,50 @@ Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'desc
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
======
{{commit_messages_str}} {{commit_messages_str}}
======
{%- endif %} {%- endif %}
{%- if question_str %} {%- if question_str %}
###### =====
Here are questions to better understand the PR. Use the answers to provide better feedback. Here are questions to better understand the PR. Use the answers to provide better feedback.
{{question_str|trim}} {{ question_str|trim }}
User answers: User answers:
' '
{{answer_str|trim}} {{ answer_str|trim }}
' '
###### =====
{%- endif %} {%- endif %}
The PR Git Diff:
``` The PR Diff:
{{diff}} ======
``` {{ diff|trim }}
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions. Focus on the '+' lines. ======
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -2,10 +2,10 @@
system=""" system="""
""" """
user="""You are given a list of code suggestions to improve a PR: user="""You are given a list of code suggestions to improve a Git Pull Request (PR):
======
{{ suggestion_str|trim }} {{ suggestion_str|trim }}
======
Your task is to sort the code suggestions by their order of importance, and return a list with sorting order. Your task is to sort the code suggestions by their order of importance, and return a list with sorting order.
The sorting order is a list of pairs, where each pair contains the index of the suggestion in the original list. The sorting order is a list of pairs, where each pair contains the index of the suggestion in the original list.

View File

@ -1,5 +1,5 @@
[pr_update_changelog_prompt] [pr_update_changelog_prompt]
system="""You are a language model called CodiumAI-PR-Changlog-summarizer. system="""You are a language model called PR-Changelog-Updater.
Your task is to update the CHANGELOG.md file of the project, to shortly summarize important changes introduced in this PR (the '+' lines). Your task is to update the CHANGELOG.md file of the project, to shortly summarize important changes introduced in this PR (the '+' lines).
- The output should match the existing CHANGELOG.md format, style and conventions, so it will look like a natural part of the file. For example, if previous changes were summarized in a single line, you should do the same. - The output should match the existing CHANGELOG.md format, style and conventions, so it will look like a natural part of the file. For example, if previous changes were summarized in a single line, you should do the same.
- Don't repeat previous changes. Generate only new content, that is not already in the CHANGELOG.md file. - Don't repeat previous changes. Generate only new content, that is not already in the CHANGELOG.md file.
@ -8,30 +8,44 @@ Your task is to update the CHANGELOG.md file of the project, to shortly summariz
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
' ======
{{ extra_instructions }} {{ extra_instructions|trim }}
' ======
{%- endif %} {%- endif %}
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
{{commit_messages_str}} ======
{{ commit_messages_str|trim }}
======
{%- endif %} {%- endif %}
The PR Diff: The PR Git Diff:
``` ======
{{diff}} {{ diff|trim }}
``` ======
Current date: Current date:
``` ```
@ -39,9 +53,10 @@ Current date:
``` ```
The current CHANGELOG.md: The current CHANGELOG.md:
``` ======
{{ changelog_file_str }} {{ changelog_file_str }}
``` ======
Response: Response:
""" """

View File

@ -1,7 +1,6 @@
import copy import copy
import textwrap import textwrap
from typing import Dict, List from typing import Dict, List
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
@ -55,9 +54,9 @@ class PRCodeSuggestions:
try: try:
get_logger().info('Generating code suggestions for PR...') get_logger().info('Generating code suggestions for PR...')
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 suggestions...", is_temporary=True)
get_logger().info('Preparing PR review...') get_logger().info('Preparing PR code suggestions...')
if not self.is_extended: if not self.is_extended:
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
data = self._prepare_pr_code_suggestions() data = self._prepare_pr_code_suggestions()
@ -73,10 +72,14 @@ class PRCodeSuggestions:
data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions'])
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info('Pushing PR review...') get_logger().info('Pushing PR code suggestions...')
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
get_logger().info('Pushing inline code suggestions...') if get_settings().pr_code_suggestions.summarize:
self.push_inline_code_suggestions(data) get_logger().info('Pushing summarize code suggestions...')
self.publish_summarizes_suggestions(data)
else:
get_logger().info('Pushing inline code suggestions...')
self.push_inline_code_suggestions(data)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}") get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
@ -116,6 +119,7 @@ class PRCodeSuggestions:
code_suggestions = [] code_suggestions = []
if not data['Code suggestions']: if not data['Code suggestions']:
get_logger().info('No suggestions found to improve this PR.')
return self.git_provider.publish_comment('No suggestions found to improve this PR.') 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']:
@ -244,4 +248,41 @@ class PRCodeSuggestions:
return data_sorted return data_sorted
def publish_summarizes_suggestions(self, data: Dict):
try:
data_markdown = "## PR Code Suggestions\n\n"
language_extension_map_org = get_settings().language_extension_map_org
extension_to_language = {}
for language, extensions in language_extension_map_org.items():
for ext in extensions:
extension_to_language[ext] = language
for s in data['Code suggestions']:
try:
extension_s = s['relevant file'].rsplit('.')[-1]
code_snippet_link = self.git_provider.get_line_link(s['relevant file'], s['relevant lines start'],
s['relevant lines end'])
data_markdown += f"\n💡 Suggestion:\n\n**{s['suggestion content']}**\n\n"
if code_snippet_link:
data_markdown += f" File: [{s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})]({code_snippet_link})\n\n"
else:
data_markdown += f"File: {s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})\n\n"
if self.git_provider.is_supported("gfm_markdown"):
data_markdown += "<details> <summary> Example code:</summary>\n\n"
data_markdown += f"___\n\n"
language_name = "python"
if extension_s and (extension_s in extension_to_language):
language_name = extension_to_language[extension_s]
data_markdown += f"Existing code:\n```{language_name}\n{s['existing code']}\n```\n"
data_markdown += f"Improved code:\n```{language_name}\n{s['improved code']}\n```\n"
if self.git_provider.is_supported("gfm_markdown"):
data_markdown += "</details>\n"
data_markdown += "\n___\n\n"
except Exception as e:
get_logger().error(f"Could not parse suggestion: {s}, error: {e}")
self.git_provider.publish_comment(data_markdown)
except Exception as e:
get_logger().info(f"Failed to publish summarized code suggestions, error: {e}")

View File

@ -30,6 +30,11 @@ class PRDescription:
) )
self.pr_id = self.git_provider.get_pr_id() self.pr_id = self.git_provider.get_pr_id()
if get_settings().pr_description.enable_semantic_files_types and not self.git_provider.is_supported(
"gfm_markdown"):
get_logger().debug(f"Disabling semantic files types for {self.pr_id}")
get_settings().pr_description.enable_semantic_files_types = False
# Initialize the AI handler # Initialize the AI handler
self.ai_handler = AiHandler() self.ai_handler = AiHandler()
@ -44,8 +49,9 @@ class PRDescription:
"extra_instructions": get_settings().pr_description.extra_instructions, "extra_instructions": get_settings().pr_description.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
"enable_custom_labels": get_settings().config.enable_custom_labels, "enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels": "", "custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
"custom_labels_examples": "", "enable_file_walkthrough": get_settings().pr_description.enable_file_walkthrough,
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types,
} }
self.user_description = self.git_provider.get_user_description() self.user_description = self.git_provider.get_user_description()
@ -80,6 +86,9 @@ class PRDescription:
else: else:
return None return None
if get_settings().pr_description.enable_semantic_files_types:
self._prepare_file_labels()
pr_labels = [] pr_labels = []
if get_settings().pr_description.publish_labels: if get_settings().pr_description.publish_labels:
pr_labels = self._prepare_labels() pr_labels = self._prepare_labels()
@ -93,14 +102,21 @@ class PRDescription:
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing answer {self.pr_id}") get_logger().info(f"Pushing answer {self.pr_id}")
if get_settings().pr_description.publish_description_as_comment: if get_settings().pr_description.publish_description_as_comment:
get_logger().info(f"Publishing answer as comment")
self.git_provider.publish_comment(full_markdown_description) self.git_provider.publish_comment(full_markdown_description)
else: else:
self.git_provider.publish_description(pr_title, pr_body) self.git_provider.publish_description(pr_title, pr_body)
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"): if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
current_labels = self.git_provider.get_labels() current_labels = self.git_provider.get_pr_labels()
user_labels = get_user_labels(current_labels) user_labels = get_user_labels(current_labels)
self.git_provider.publish_labels(pr_labels + user_labels) self.git_provider.publish_labels(pr_labels + user_labels)
if (get_settings().pr_description.final_update_message and
hasattr(self.git_provider, 'pr_url') and self.git_provider.pr_url):
latest_commit_url = self.git_provider.get_latest_commit_url()
if latest_commit_url:
self.git_provider.publish_comment(
f"**[PR Description]({self.git_provider.pr_url})** updated to latest commit ({latest_commit_url})")
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
except Exception as e: except Exception as e:
get_logger().error(f"Error generating PR description {self.pr_id}: {e}") get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
@ -143,7 +159,7 @@ class PRDescription:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables) set_custom_labels(variables, self.git_provider)
system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables)
@ -175,16 +191,16 @@ class PRDescription:
pr_types = [] pr_types = []
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types' # If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'PR Labels' in self.data: if 'labels' in self.data:
if type(self.data['PR Labels']) == list: if type(self.data['labels']) == list:
pr_types = self.data['PR Labels'] pr_types = self.data['labels']
elif type(self.data['PR Labels']) == str: elif type(self.data['labels']) == str:
pr_types = self.data['PR Labels'].split(',') pr_types = self.data['labels'].split(',')
elif 'PR Type' in self.data: elif 'type' in self.data:
if type(self.data['PR Type']) == list: if type(self.data['type']) == list:
pr_types = self.data['PR Type'] pr_types = self.data['type']
elif type(self.data['PR Type']) == str: elif type(self.data['type']) == str:
pr_types = self.data['PR Type'].split(',') pr_types = self.data['type'].split(',')
return pr_types return pr_types
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]: def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]:
@ -196,12 +212,12 @@ class PRDescription:
else: else:
ai_header = "" ai_header = ""
ai_type = self.data.get('PR Type') ai_type = self.data.get('type')
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body): if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
pr_type = f"{ai_header}{ai_type}" pr_type = f"{ai_header}{ai_type}"
body = body.replace('pr_agent:type', pr_type) body = body.replace('pr_agent:type', pr_type)
ai_summary = self.data.get('PR Description') ai_summary = self.data.get('description')
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body): if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
summary = f"{ai_header}{ai_summary}" summary = f"{ai_header}{ai_summary}"
body = body.replace('pr_agent:summary', summary) body = body.replace('pr_agent:summary', summary)
@ -231,16 +247,16 @@ class PRDescription:
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format # Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = "" markdown_text = ""
# Don't display 'PR Labels' # Don't display 'PR Labels'
if 'PR Labels' in self.data and self.git_provider.is_supported("get_labels"): if 'labels' in self.data and self.git_provider.is_supported("get_labels"):
self.data.pop('PR Labels') self.data.pop('labels')
if not get_settings().pr_description.enable_pr_type: if not get_settings().pr_description.enable_pr_type:
self.data.pop('PR Type') self.data.pop('type')
for key, value in self.data.items(): for key, value in self.data.items():
markdown_text += f"## {key}\n\n" markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n" markdown_text += f"{value}\n\n"
# Remove the 'PR Title' key from the dictionary # Remove the 'PR Title' key from the dictionary
ai_title = self.data.pop('PR Title', self.vars["title"]) ai_title = self.data.pop('title', self.vars["title"])
if get_settings().pr_description.keep_original_user_title: if get_settings().pr_description.keep_original_user_title:
# Assign the original PR title to the 'title' variable # Assign the original PR title to the 'title' variable
title = self.vars["title"] title = self.vars["title"]
@ -252,26 +268,130 @@ class PRDescription:
# except for the items containing the word 'walkthrough' # except for the items containing the word 'walkthrough'
pr_body = "" pr_body = ""
for idx, (key, value) in enumerate(self.data.items()): for idx, (key, value) in enumerate(self.data.items()):
pr_body += f"## {key}:\n" if key == 'pr_files':
value = self.file_label_dict
key_publish = "PR changes walkthrough"
else:
key_publish = key.rstrip(':').replace("_", " ").capitalize()
pr_body += f"## {key_publish}\n"
if 'walkthrough' in key.lower(): if 'walkthrough' in key.lower():
# for filename, description in value.items():
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
pr_body += "<details> <summary>files:</summary>\n\n" pr_body += "<details> <summary>files:</summary>\n\n"
for file in value: for file in value:
filename = file['filename'].replace("'", "`") filename = file['filename'].replace("'", "`")
description = file['changes in file'] description = file['changes_in_file']
pr_body += f'- `{filename}`: {description}\n' pr_body += f'- `{filename}`: {description}\n'
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
pr_body +="</details>\n" pr_body += "</details>\n"
elif 'pr_files' in key.lower():
pr_body = self.process_pr_files_prediction(pr_body, value)
else: else:
# if the value is a list, join its items by comma # if the value is a list, join its items by comma
if type(value) == list: if isinstance(value, list):
value = ', '.join(v for v in value) value = ', '.join(v for v in value)
pr_body += f"{value}\n" pr_body += f"{value}\n"
if idx < len(self.data) - 1: if idx < len(self.data) - 1:
pr_body += "\n___\n" pr_body += "\n\n___\n\n"
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"title:\n{title}\n{pr_body}") get_logger().info(f"title:\n{title}\n{pr_body}")
return title, pr_body return title, pr_body
def _prepare_file_labels(self):
self.file_label_dict = {}
for file in self.data['pr_files']:
try:
filename = file['filename'].replace("'", "`").replace('"', '`')
changes_summary = file['changes_summary']
label = file['label']
if label not in self.file_label_dict:
self.file_label_dict[label] = []
self.file_label_dict[label].append((filename, changes_summary))
except Exception as e:
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
pass
def process_pr_files_prediction(self, pr_body, value):
if not self.git_provider.is_supported("gfm_markdown"):
get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported")
return pr_body
try:
pr_body += "<table>"
header = f"Relevant files"
delta = 65
header += "&nbsp; " * delta
pr_body += f"""<thead><tr><th></th><th>{header}</th></tr></thead>"""
pr_body += """<tbody>"""
for semantic_label in value.keys():
s_label = semantic_label.strip("'").strip('"')
pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>"""
list_tuples = value[semantic_label]
pr_body += f"""<td><details><summary>{len(list_tuples)} files</summary><table>"""
for filename, file_change_description in list_tuples:
filename = filename.replace("'", "`")
filename_publish = filename.split("/")[-1]
filename_publish = f"{filename_publish}"
if len(filename_publish) < (delta - 5):
filename_publish += "&nbsp; " * ((delta - 5) - len(filename_publish))
diff_plus_minus = ""
diff_files = self.git_provider.diff_files
for f in diff_files:
if f.filename.lower() == filename.lower():
num_plus_lines = f.num_plus_lines
num_minus_lines = f.num_minus_lines
diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}"
break
# try to add line numbers link to code suggestions
link = ""
if hasattr(self.git_provider, 'get_line_link'):
filename = filename.strip()
link = self.git_provider.get_line_link(filename, relevant_line_start=-1)
file_change_description = self._insert_br_after_x_chars(file_change_description, x=(delta - 5))
pr_body += f"""
<tr>
<td>
<details>
<summary><strong>{filename_publish}</strong></summary>
<ul>
{filename}<br><br>
<strong>{file_change_description}</strong>
</ul>
</details>
</td>
<td><a href="{link}"> {diff_plus_minus}</a></td>
</tr>
"""
pr_body += """</table></details></td></tr>"""
pr_body += """</tr></tbody></table>"""
except Exception as e:
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {e}")
pass
return pr_body
def _insert_br_after_x_chars(self, text, x=70):
"""
Insert <br> into a string after a word that increases its length above x characters.
"""
if len(text) < x:
return text
words = text.split(' ')
new_text = ""
current_length = 0
for word in words:
# Check if adding this word exceeds x characters
if current_length + len(word) > x:
new_text += "<br>" # Insert line break
current_length = 0 # Reset counter
# Add the word to the new text
new_text += word + " "
current_length += len(word) + 1 # Add 1 for the space
return new_text.strip() # Remove trailing space

View File

@ -43,9 +43,8 @@ class PRGenerateLabels:
"use_bullet_points": get_settings().pr_description.use_bullet_points, "use_bullet_points": get_settings().pr_description.use_bullet_points,
"extra_instructions": get_settings().pr_description.extra_instructions, "extra_instructions": get_settings().pr_description.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
"custom_labels": "",
"custom_labels_examples": "",
"enable_custom_labels": get_settings().config.enable_custom_labels, "enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
} }
# Initialize the token handler # Initialize the token handler
@ -83,7 +82,7 @@ class PRGenerateLabels:
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing labels {self.pr_id}") get_logger().info(f"Pushing labels {self.pr_id}")
current_labels = self.git_provider.get_labels() current_labels = self.git_provider.get_pr_labels()
user_labels = get_user_labels(current_labels) user_labels = get_user_labels(current_labels)
pr_labels = pr_labels + user_labels pr_labels = pr_labels + user_labels
@ -133,7 +132,7 @@ class PRGenerateLabels:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables) set_custom_labels(variables, self.git_provider)
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables)
@ -148,6 +147,9 @@ class PRGenerateLabels:
user=user_prompt user=user_prompt
) )
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nAI response:\n{response}")
return response return response
def _prepare_data(self): def _prepare_data(self):
@ -159,11 +161,11 @@ class PRGenerateLabels:
def _prepare_labels(self) -> List[str]: def _prepare_labels(self) -> List[str]:
pr_types = [] pr_types = []
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types' # If the 'labels' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'PR Type' in self.data: if 'labels' in self.data:
if type(self.data['PR Type']) == list: if type(self.data['labels']) == list:
pr_types = self.data['PR Type'] pr_types = self.data['labels']
elif type(self.data['PR Type']) == str: elif type(self.data['labels']) == str:
pr_types = self.data['PR Type'].split(',') pr_types = self.data['labels'].split(',')
return pr_types return pr_types

View File

@ -249,11 +249,15 @@ class PRReviewer:
# Add help text if not in CLI mode # Add help text if not in CLI mode
if not get_settings().get("CONFIG.CLI_MODE", False): if not get_settings().get("CONFIG.CLI_MODE", False):
markdown_text += "\n### How to use\n" markdown_text += "\n### How to use\n"
if self.git_provider.is_supported("gfm_markdown"):
markdown_text += "\n <details> <summary> Instructions</summary>\n\n"
bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user
if user and bot_user not in user: if user and bot_user not in user:
markdown_text += bot_help_text(user) markdown_text += bot_help_text(user)
else: else:
markdown_text += actions_help_text markdown_text += actions_help_text
if self.git_provider.is_supported("gfm_markdown"):
markdown_text += "\n</details>\n"
# Add custom labels from the review prediction (effort, security) # Add custom labels from the review prediction (effort, security)
self.set_review_labels(data) self.set_review_labels(data)
@ -274,14 +278,7 @@ class PRReviewer:
if get_settings().pr_reviewer.num_code_suggestions == 0: if get_settings().pr_reviewer.num_code_suggestions == 0:
return return
review_text = self.prediction.strip() data = load_yaml(self.prediction.strip())
review_text = review_text.removeprefix('```yaml').rstrip('`')
try:
data = yaml.load(review_text, Loader=SafeLoader)
except Exception as e:
get_logger().error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text)
comments: List[str] = [] comments: List[str] = []
for suggestion in data.get('PR Feedback', {}).get('Code feedback', []): for suggestion in data.get('PR Feedback', {}).get('Code feedback', []):
relevant_file = suggestion.get('relevant file', '').strip() relevant_file = suggestion.get('relevant file', '').strip()
@ -395,11 +392,12 @@ class PRReviewer:
if security_concerns_bool: if security_concerns_bool:
review_labels.append('Possible security concern') review_labels.append('Possible security concern')
if review_labels: current_labels = self.git_provider.get_pr_labels()
current_labels = self.git_provider.get_labels() current_labels_filtered = [label for label in current_labels if
current_labels_filtered = [label for label in current_labels if not label.lower().startswith('review effort [1-5]:') and not label.lower().startswith(
not label.lower().startswith('review effort [1-5]:') and not label.lower().startswith( 'possible security concern')]
'possible security concern')] if current_labels or review_labels:
get_logger().info(f"Setting review labels: {review_labels + current_labels_filtered}")
self.git_provider.publish_labels(review_labels + current_labels_filtered) self.git_provider.publish_labels(review_labels + current_labels_filtered)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to set review labels, error: {e}") get_logger().error(f"Failed to set review labels, error: {e}")

View File

@ -8,6 +8,7 @@ import pinecone
from pinecone_datasets import Dataset, DatasetMetadata from pinecone_datasets import Dataset, DatasetMetadata
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pr_agent.algo import MAX_TOKENS
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import get_max_tokens from pr_agent.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings

View File

@ -1,25 +1,25 @@
dynaconf==3.1.12 aiohttp==3.9.1
fastapi==0.99.0
PyGithub==1.59.*
retry==0.9.2
openai==0.27.8
Jinja2==3.1.2
tiktoken==0.4.0
uvicorn==0.22.0
python-gitlab==3.15.0
pytest==7.4.0
aiohttp==3.8.4
atlassian-python-api==3.39.0 atlassian-python-api==3.39.0
GitPython==3.1.32
PyYAML==6.0
starlette-context==0.3.6
litellm==0.12.5
boto3==1.28.25
google-cloud-storage==2.10.0
ujson==5.8.0
azure-devops==7.1.0b3 azure-devops==7.1.0b3
boto3==1.33.6
dynaconf==3.2.4
fastapi==0.99.0
GitPython==3.1.32
google-cloud-aiplatform==1.35.0
google-cloud-storage==2.10.0
Jinja2==3.1.2
litellm==0.12.5
loguru==0.7.2
msrest==0.7.1 msrest==0.7.1
openai==0.27.8
pinecone-client pinecone-client
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
loguru==0.7.2 pytest==7.4.0
google-cloud-aiplatform==1.35.0 PyGithub==1.59.*
PyYAML==6.0.1
python-gitlab==3.15.0
retry==0.9.2
starlette-context==0.3.6
tiktoken==0.5.2
ujson==5.8.0
uvicorn==0.22.0

View File

@ -1,3 +1,4 @@
from pr_agent.git_providers import BitbucketServerProvider
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
@ -8,3 +9,10 @@ class TestBitbucketProvider:
assert workspace_slug == "WORKSPACE_XYZ" assert workspace_slug == "WORKSPACE_XYZ"
assert repo_slug == "MY_TEST_REPO" assert repo_slug == "MY_TEST_REPO"
assert pr_number == 321 assert pr_number == 321
def test_bitbucket_server_pr_url(self):
url = "https://git.onpreminstance.com/projects/AAA/repos/my-repo/pull-requests/1"
workspace_slug, repo_slug, pr_number = BitbucketServerProvider._parse_pr_url(url)
assert workspace_slug == "AAA"
assert repo_slug == "my-repo"
assert pr_number == 1

View File

@ -0,0 +1,19 @@
# Generated by CodiumAI
import pytest
from pr_agent.algo.utils import clip_tokens
class TestClipTokens:
def test_clip(self):
text = "line1\nline2\nline3\nline4\nline5\nline6"
max_tokens = 25
result = clip_tokens(text, max_tokens)
assert result == text
max_tokens = 10
result = clip_tokens(text, max_tokens)
expected_results = 'line1\nline2\nline3\nli...(truncated)'
assert result == expected_results

View File

@ -71,7 +71,7 @@ class TestConvertToMarkdown:
- 📌 **Type of PR:** Test type\n\ - 📌 **Type of PR:** Test type\n\
- 🧪 **Relevant tests added:** no\n\ - 🧪 **Relevant tests added:** no\n\
- ✨ **Focused PR:** Yes\n\ - ✨ **Focused PR:** Yes\n\
- **General PR suggestions:** general suggestion...\n\n\n- **<details><summary> 🤖 Code feedback:**</summary>\n\n - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n</details>\ - **General PR suggestions:** general suggestion...\n\n\n- <details><summary> 🤖 Code feedback:</summary> - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n</details>\
""" """
assert convert_to_markdown(input_data).strip() == expected_output.strip() assert convert_to_markdown(input_data).strip() == expected_output.strip()

View File

@ -2,6 +2,9 @@
# Generated by CodiumAI # Generated by CodiumAI
import pytest import pytest
import yaml
from yaml.scanner import ScannerError
from pr_agent.algo.utils import load_yaml from pr_agent.algo.utils import load_yaml
@ -12,7 +15,7 @@ class TestLoadYaml:
expected_output = {'name': 'John Smith', 'age': 35} expected_output = {'name': 'John Smith', 'age': 35}
assert load_yaml(yaml_str) == expected_output assert load_yaml(yaml_str) == expected_output
def test_load_complicated_yaml(self): def test_load_invalid_yaml1(self):
yaml_str = \ yaml_str = \
'''\ '''\
PR Analysis: PR Analysis:
@ -26,7 +29,23 @@ PR Feedback:
Code feedback: Code feedback:
- relevant file: pr_agent/settings/pr_description_prompts.toml - 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] 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:' relevant line: user="""PR Info: aaa
Security concerns: No''' 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}} with pytest.raises(ScannerError):
yaml.safe_load(yaml_str)
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: aaa'}], 'Security concerns': False}}
assert load_yaml(yaml_str) == expected_output assert load_yaml(yaml_str) == expected_output
def test_load_invalid_yaml2(self):
yaml_str = '''\
- relevant file: src/app.py:
suggestion content: The print statement is outside inside the if __name__ ==: \
'''
with pytest.raises(ScannerError):
yaml.safe_load(yaml_str)
expected_output =[{'relevant file': 'src/app.py:',
'suggestion content': 'The print statement is outside inside the if __name__ ==: '}]
assert load_yaml(yaml_str) == expected_output

View File

@ -61,7 +61,7 @@ class TestParseCodeSuggestion:
'before': 'Before 1', 'before': 'Before 1',
'after': 'After 1' 'after': 'After 1'
} }
expected_output = " **suggestion:** Suggestion 1\n **description:** Description 1\n **before:** Before 1\n **after:** After 1\n\n" # noqa: E501 expected_output = ' **suggestion:** Suggestion 1 \n **description:** Description 1 \n **before:** Before 1 \n **after:** After 1 \n\n' # noqa: E501
assert parse_code_suggestion(code_suggestions) == expected_output assert parse_code_suggestion(code_suggestions) == expected_output
# Tests that function returns correct output when input dictionary has 'code example' key # Tests that function returns correct output when input dictionary has 'code example' key
@ -74,5 +74,5 @@ class TestParseCodeSuggestion:
'after': 'After 2' 'after': 'After 2'
} }
} }
expected_output = " **suggestion:** Suggestion 2\n **description:** Description 2\n - **code example:**\n - **before:**\n ```\n Before 2\n ```\n - **after:**\n ```\n After 2\n ```\n\n" # noqa: E501 expected_output = ' **suggestion:** Suggestion 2 \n **description:** Description 2 \n - **code example:**\n - **before:**\n ```\n Before 2\n ```\n - **after:**\n ```\n After 2\n ```\n\n' # noqa: E501
assert parse_code_suggestion(code_suggestions) == expected_output assert parse_code_suggestion(code_suggestions) == expected_output