Compare commits

...

679 Commits

Author SHA1 Message Date
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
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
416a5495da Merge pull request #453 from Codium-ai/tr/v_010
Version 0.10 Release and Workflow Update
2023-11-15 09:46:31 -08:00
a2b27dcac8 v10 2023-11-15 19:45:51 +02:00
d8e4e2e8fd Merge pull request #454 from Codium-ai/coditamar-bitbucket-doc-type
Update INSTALL.md
2023-11-15 09:44:03 -08:00
3fae5cbd8d feat: Added BitBucket Server
Signed-off-by: Luca Simone <info@lucasimone.info>
2023-11-15 15:47:44 +01:00
896a81d173 Update INSTALL.md 2023-11-15 15:20:50 +02:00
b216af8f04 v10 2023-11-15 14:49:18 +02:00
388cc740b6 Merge pull request #436 from rhyst/support-vertex-ai
Support Google's Vertex AI
2023-11-15 04:26:08 -08:00
6214494c84 Merge pull request #452 from Codium-ai/tr/review_extra_labels
Add Review Labels for Security and Effort Estimation
2023-11-15 04:25:03 -08:00
762a6981e1 extra_labels 2023-11-15 14:12:59 +02:00
b362c406bc Merge remote-tracking branch 'origin/main' into tr/review_extra_labels 2023-11-15 14:07:44 +02:00
7a342d3312 extra_labels 2023-11-15 14:07:32 +02:00
2e95988741 extra_labels 2023-11-15 14:04:17 +02:00
9478447141 extra_labels 2023-11-15 14:02:13 +02:00
082293b48c Merge pull request #451 from Codium-ai/tr/persistent_enhacments
Enhancement of Persistent Comments in PR Review
2023-11-15 03:55:15 -08:00
e1d92206f3 docs 2023-11-15 13:32:32 +02:00
557ec72bfe Update documentation for Vertex AI 2023-11-15 10:27:48 +00:00
6b4b16dcf9 Support Google's Vertex AI 2023-11-15 10:26:58 +00:00
c4899a6c54 bitbucket 2023-11-15 12:11:02 +02:00
24d82e65cb gitlab 2023-11-15 09:45:10 +02:00
2567a6cf27 gitlab 2023-11-15 09:40:45 +02:00
94cb6b9795 more feedback 2023-11-15 09:06:26 +02:00
e878bbbe36 Merge pull request #449 from zmeir/patch-1
Fix `get_user_description` in case `pr_description.enable_pr_type=false`
2023-11-14 22:09:59 -08: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
7d89b82967 Fix get_user_description in case pr_description.enable_pr_type=false
Fixes an issue when getting the user description after a PR-Agent description was already generated, in case the configuration setting `pr_description.enable_pr_type` was `false`.
2023-11-13 14:41:14 +02:00
c5f9bbbf92 Merge pull request #448 from Codium-ai/hl/optional_custom_labels
remove the "one or more" for custom labels
2023-11-13 13:51:18 +02:00
a5e5a82952 s 2023-11-13 13:49:16 +02:00
ccbb62b50a remove the "one or more" for custom labels 2023-11-13 13:47:06 +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
1df36c6a44 Merge pull request #446 from Codium-ai/tr/fix_cli_args
Handling CLI Arguments with Quotes in pr_agent
2023-11-12 17:29:38 +02:00
e9891fc530 s1 2023-11-12 16:37:53 +02:00
9e5e9afe92 Refactor CLI argument handling and request processing 2023-11-12 16:11:34 +02:00
5e43c202dd s1 2023-11-12 15:45:22 +02:00
727eea2b62 s1 2023-11-12 15:00:06 +02:00
37e6608e68 Merge pull request #444 from Codium-ai/tr/fallback_yaml
Implementing Fallback Mechanisms for YAML Parsing
2023-11-12 00:43:15 -08:00
f64d5f1e2a tests 2023-11-12 08:36:57 +02:00
8fdf174dec fallback 2023-11-10 18:44:19 +02:00
29d4f98b19 Merge pull request #441 from Codium-ai/tr/presistent_review
Add Persistent Review Feature to PR Agent
2023-11-09 05:26:51 -08:00
737792d83c publish_persistent_comment 2023-11-09 15:24:55 +02:00
7e5889061c publish_persistent_comment 2023-11-09 15:20:31 +02:00
755e04cf65 bitbucket finally works 2023-11-08 20:41:55 +02:00
44d6c95714 response 2023-11-08 20:38:18 +02:00
14610d5375 persistent
s
2023-11-08 20:16:08 +02:00
f9c832d6cb Merge pull request #439 from Codium-ai/tr/fixes_added_files
Enhancement of Patch Handling and PR Processing
2023-11-08 04:48:07 -08:00
c2bec614e5 s 2023-11-08 14:46:11 +02:00
49725e92f2 s 2023-11-08 14:41:15 +02:00
a1e32d8331 s 2023-11-08 14:36:59 +02:00
0293412a42 s 2023-11-08 14:31:08 +02:00
10ec0a1812 s 2023-11-08 14:21:03 +02:00
69b68b78f5 s 2023-11-08 14:17:59 +02:00
c5bc4b44ff fix added files 2023-11-08 12:51:30 +02:00
39e5102a2e fix added files 2023-11-08 12:47:18 +02:00
f0991526b5 remove unnecessary setup_logger 2023-11-08 16:56:44 +09:00
6c82bc9a3e Merge pull request #437 from Codium-ai/tr/new_gpt4
Introduce support for 'gpt-4-1106-preview' model and dynamic token limit calculation
2023-11-07 04:49:50 -08:00
54f41dd603 code 2023-11-07 14:41:15 +02:00
094f641fb5 code 2023-11-07 14:38:37 +02:00
a35a75eb34 get_max_tokens + added 'gpt-4-1106-preview' 2023-11-07 14:28:41 +02:00
5a7c118b56 Merge pull request #434 from Codium-ai/document_describe
Update DESCRIBE.md
2023-11-06 11:00:06 -08:00
cf9e0fbbc5 Update DESCRIBE.md 2023-11-06 17:55:58 +02:00
ef9af261ed Merge pull request #433 from Codium-ai/hl/user_labels
Keep user labels
2023-11-06 15:17:19 +02:00
ff79776410 Keep user labels 2023-11-06 15:14:08 +02:00
ec3f2fb485 Revert "generate labels keep user labels only"
This reverts commit 94a2a5e527.
2023-11-06 15:08:29 +02:00
94a2a5e527 generate labels keep user labels only 2023-11-06 15:07:06 +02:00
ea4bc548fc Merge pull request #432 from Codium-ai/hl/type_vs_labels
Support git providers with no label support
2023-11-06 14:38:29 +02:00
1eefd3365b Merge commit 'e352c98ce83bfbd99078f62d8705eb938a6ba5b5' into hl/type_vs_labels 2023-11-06 14:24:33 +02:00
db37ee819a support git providers with no label support 2023-11-06 14:11:49 +02:00
e352c98ce8 Merge pull request #431 from Codium-ai/hl/type_vs_labels
Refactoring PR Labels Handling and Display
2023-11-06 02:10:38 -08:00
e96b03da57 add configuration enable_pr_type 2023-11-06 11:58:26 +02:00
1d2aedf169 Don't Display pr labels in the text 2023-11-06 11:35:22 +02:00
4c484f8e86 Merge pull request #423 from zmeir/zmeir-external-incremental_review_thresholds
Implementing Thresholds for Incremental PR Reviews
2023-11-06 01:07:01 -08:00
8a79114ed9 Merge pull request #430 from Codium-ai/tr/fix_prompt
Fix PR Description Prompt and Data Preparation
2023-11-06 01:06:26 -08:00
cd69f43c77 Merge pull request #428 from Codium-ai/tr/fixes
Enhancements and Fixes in Bitbucket Provider
2023-11-06 01:06:15 -08:00
6d6d864417 fix prompt 2023-11-06 09:44:59 +02:00
b286c8ed20 Added documentation for the new configurations 2023-11-06 09:44:04 +02:00
7238c81f0c fix prompt 2023-11-06 09:41:26 +02:00
62412f8cd4 fix prompt 2023-11-06 09:38:39 +02:00
5d2bdadb45 fix prompt 2023-11-06 09:33:10 +02:00
06d030637c fix prompt 2023-11-06 09:32:46 +02:00
8e3fa3926a Extract incremental review checks to separate method 2023-11-06 09:21:22 +02:00
92071fcf1c Stack all incremental parameters 2023-11-06 09:13:04 +02:00
fed1c160eb files walkthrough bullets 2023-11-06 08:43:15 +02:00
e37daf6987 link to change 2023-11-06 08:27:34 +02:00
8fc663911f fixe bitbucket get_repo_settings bug 2023-11-06 08:15:43 +02:00
bb2760ae41 tools 2023-11-06 08:10:04 +02:00
3548b88463 type and labels 2023-11-05 15:48:39 +02:00
c917e48098 Merge pull request #427 from koid/fix/add-middleware
Adding Middleware to FastAPI Initialization
2023-11-05 01:40:37 -07:00
e6ef123ce5 add middleware when initializing fastapi 2023-11-05 15:38:19 +09:00
194bfe1193 Update INSTALL.md 2023-11-05 07:59:59 +02:00
e456cf36aa Merge pull request #425 from Codium-ai/ok/protect_apply_settings
Add exception handling for applying repo settings failure
2023-11-03 11:07:49 -07:00
fe3527de3c Add exception handling for applying repo settings failure 2023-11-03 12:23:49 +02:00
b99c769b53 Merge pull request #415 from zmeir/zmeir-patch-2
Refactor Command Handling for Different Triggers
2023-11-02 18:32:42 +02:00
60bdfb78df Merge pull request #424 from Codium-ai/ok/bitbucket_fix
Update Bitbucket Provider to Use 'position' Instead of 'start_line' for Inline Comments
2023-11-02 18:31:18 +02:00
c0b3c76884 Merge remote-tracking branch 'origin/main' into ok/bitbucket_fix 2023-11-02 15:27:11 +02:00
e1370a8385 Update publish_inline_comments in bitbucket_provider.py to use 'position' instead of 'start_line' 2023-11-02 15:24:47 +02:00
c623c3baf4 Added new configurations to prevent too frequent incremental commits on push trigger 2023-11-02 12:24:54 +02:00
d0f3a4139d Merge pull request #422 from Codium-ai/pr_review_fix_type_example
small fix to pr type example yaml
2023-11-02 11:48:49 +02:00
3ddc7e79d1 Update pr_reviewer_prompts.toml 2023-11-02 11:45:34 +02:00
3e14edfd4e Merge pull request #421 from zmeir/patch-1
Fix error in `get_main_pr_languages` when the diff is empty
2023-11-02 01:20:36 -07:00
15573e2286 Fix error in get_main_pr_languages when the diff is empty
This can happen for example when you have one commit add a line to a file and the next commit deletes that line. Then if those are the only 2 commits in the PR the diff will be empty.
2023-11-02 10:10:54 +02:00
ce64877063 Merge pull request #419 from KennyDizi/fix_synstax_error
Fix Syntax Error in f-string Expression
2023-11-02 08:23:17 +02:00
6666a128ee Update Usage.md 2023-11-01 18:36:12 +02:00
9fbf89670d Improve expression portion of f-strings 2023-11-01 19:11:52 +07:00
ad1c51c536 Fix SyntaxError: f-string expression part cannot include a backslash 2023-11-01 19:06:29 +07:00
9ab7ccd20d Merge pull request #416 from zmeir/patch-1
Fix formatting when last commit message contains _
2023-11-01 13:13:31 +02:00
c907f93ab8 Merge pull request #418 from KennyDizi/fix/configuration-typo
Fix Typo and Update Comment for Ollama Configuration
2023-10-31 11:14:51 -07:00
29a8cf8357 fix typo for ollama 2023-10-31 20:38:27 +07:00
7b6a6c7164 Fix formatting when last commit message contains _ 2023-10-31 10:05:13 +02:00
cf4d007737 Fix commands list for push trigger 2023-10-31 00:00:48 +02:00
a751bb0ef0 Merge pull request #414 from Codium-ai/ok/fix_github_bug
Bugfix: ignored github_app.commands on .pr-agent.toml
2023-10-30 20:28:54 +02:00
26d6280a20 Merge remote-tracking branch 'origin/main' into ok/fix_github_bug 2023-10-30 20:19:41 +02:00
32a19fdab6 Merge pull request #413 from Codium-ai/ok/bitbucket_repo_settings
Update Method to Fetch Repository Settings in bitbucket_provider.py
2023-10-30 20:18:59 +02:00
775ccb3f25 Refactor _perform_commands function in github_app.py to improve command handling 2023-10-30 20:14:25 +02:00
a1c6c57f7b Merge remote-tracking branch 'origin/main' into ok/bitbucket_repo_settings 2023-10-30 18:38:08 +02:00
73bb70fef4 Update get_repo_settings in bitbucket_provider.py to fetch file via API request 2023-10-30 18:36:46 +02:00
dcac6c145c Merge pull request #412 from Codium-ai/ok/fix_gitlab_bug
Update get_repo_settings to decode file from target branch in gitlab_…
2023-10-30 17:04:09 +02:00
4bda9dfe04 Update get_repo_settings to decode file from target branch in gitlab_provider.py 2023-10-30 17:01:49 +02:00
66644f0224 Merge pull request #411 from Codium-ai/ok/fix_gitlab_bug
Add Logging Context to Handle Request Calls in gitlab_webhook.py
2023-10-30 16:53:40 +02:00
e74bb80668 Refactor get_repo_settings method in gitlab_provider.py to decode file contents 2023-10-30 16:45:47 +02:00
e06fb534d3 Merge remote-tracking branch 'origin/main' into ok/fix_gitlab_bug 2023-10-30 16:34:03 +02:00
71a341855e Add log_context to handle_request calls in gitlab_webhook.py 2023-10-30 16:00:09 +02:00
7d949ad6e2 Update GENERATE_CUSTOM_LABELS.md 2023-10-30 15:20:17 +02:00
4b5f86fcf0 Merge pull request #410 from Codium-ai/fix_link_in_install
small fix in link in install.md
2023-10-30 02:58:52 -07:00
cd11f51df0 small fix in link in install.md 2023-10-30 11:47:24 +02:00
b40c0b9b23 Merge pull request #409 from zmeir/patch-1
Fix call to `_get_previous_review_comment`
2023-10-30 01:28:12 -07:00
816ddeeb9e Fix call to _get_previous_review_comment
Hey @mrT23, I thinks there's a problem with moving this line to after `self.git_provider.publish_comment(pr_comment)`.

The reason I originally placed it here is because otherwise, if you run `/review --pr_reviewer.remove_previous_review_comment=true` it will publish your review and then immediately after delete it, because it will look for the previous review comment only after you published your new review - so it will take your new review as the previous one. In order to get the real "previous" review you must collect the comments list before publishing a review, so placing this method call first ensures that.

The method `self._get_previous_review_comment()` is a no-op if `pr_reviewer.remove_previous_review_comment=false` so I see no downside in keeping it before `self.git_provider.publish_comment(pr_comment)`

Additionally, the check for `if previous_review_comment:` is redundant because it's done internally in `self._remove_previous_review_comment`. I thought it looked cleaner without this extra nesting here, but if you think more verbosity is better I'll keep it.
2023-10-30 09:06:51 +02:00
11f01a226c Update RELEASE_NOTES.md 2023-10-30 08:22:36 +02:00
b57ec301e8 Merge pull request #408 from Codium-ai/tr/final_fixes
fixed review
2023-10-29 09:02:48 -07:00
71da20ea7e better link 2023-10-29 18:01:50 +02:00
c895657310 fixed review 2023-10-29 17:59:46 +02:00
eda20ccca9 Merge pull request #407 from zmeir/patch-1
Update Usage.md with new GitHub App features
2023-10-29 16:54:59 +02:00
aed113cd79 Update Usage.md with new GitHub App features 2023-10-29 16:33:38 +02:00
0ab07a46c6 Merge pull request #405 from Codium-ai/tr/final_fixes
Final Fixes and Updates to PR Agent
2023-10-29 06:02:34 -07:00
5f32e28933 generate_labels 2023-10-29 15:02:16 +02:00
7538c4dd2f generate_labels 2023-10-29 14:59:50 +02:00
e3845283f8 release notes 2023-10-29 14:58:36 +02:00
a85921d3c5 release notes 2023-10-29 14:49:35 +02:00
27b64fbcaf release notes 2023-10-29 14:47:46 +02:00
8d50f2ae82 release notes 2023-10-29 14:43:45 +02:00
e97a03f522 Merge remote-tracking branch 'origin/main' into tr/final_fixes 2023-10-29 14:38:33 +02:00
2e3344b5b0 Merge pull request #406 from Codium-ai/hl/custom_labels
Add documentation to custom labels
2023-10-29 05:38:11 -07:00
e1b51eace7 release notes 2023-10-29 14:37:04 +02:00
49e3d5ec5f Add documentation 2023-10-29 13:58:01 +02:00
afa78ed3fb final fixes 2023-10-29 13:07:22 +02:00
72d5e4748e final fixes 2023-10-29 13:05:15 +02:00
61d3e1ebf4 Merge pull request #394 from zmeir/zmeir-external-push_trigger
Added support for automatic review on push event
2023-10-29 13:04:33 +02:00
055b5ea700 final fixes 2023-10-29 13:03:12 +02:00
3434296792 Documentation 2023-10-29 13:02:07 +02:00
ae375c2ff0 final fixes 2023-10-29 13:01:55 +02:00
3d5efdf4f3 Merge commit '9a585de36461a6941cb77009e5ab5f4b568a1ff7' into hl/custom_labels 2023-10-29 13:01:53 +02:00
9a585de364 Merge pull request #404 from Codium-ai/tr/final_fixes
final fixes
2023-10-29 12:31:40 +02:00
c27dc436c4 final fixes 2023-10-29 12:29:14 +02:00
e83747300d Merge branch 'main' of github.com:Codium-ai/pr-agent into hl/custom_labels 2023-10-29 12:09:43 +02:00
7374243d0b enable_custom_labels 2023-10-29 11:40:36 +02:00
5c568bc0c5 Merge pull request #403 from Codium-ai/tr/fix_custom_labels
Refactoring Custom Labels Handling and Documentation Update
2023-10-29 02:34:18 -07:00
22c196cb3b Merge remote-tracking branch 'origin/main' into tr/fix_custom_labels
# Conflicts:
#	pr_agent/git_providers/github_provider.py
2023-10-29 10:58:42 +02:00
d2cc856cfc Merge pull request #402 from Codium-ai/tr/github_action_uses_toml
Update GitHub Action to Use .pr_agent.toml
2023-10-29 01:55:33 -07:00
013a689b33 generate_labels fix 2023-10-29 10:43:04 +02:00
d772213cfc fix labels 2023-10-29 08:58:12 +02:00
638db96311 github action now also uses .pr_agent.toml 2023-10-28 13:34:32 +03:00
4dffabf397 Merge pull request #396 from Codium-ai/hl/custom_labels
Implement Custom Labels for PRs
2023-10-28 01:37:54 +03:00
6f2bbd3baa Add documentation 2023-10-28 00:45:59 +03:00
9e41f3780c disable custom labels by default 2023-10-27 21:22:56 +03:00
f53ec1d0cc move enable custom labels to custom labels function 2023-10-27 21:12:58 +03:00
f7666cb59a Update INSTALL.md 2023-10-27 11:49:39 +03:00
a7cb59ca8b small fix 2023-10-27 08:10:29 +03:00
ca0ea77415 refactor 2023-10-27 07:58:42 +03:00
0cf27e5fee custom labels disabled by default 2023-10-27 07:54:59 +03:00
f3bdbfc103 Add /generate_labels function + fix issues 2023-10-26 23:28:33 +03:00
20e3acdd86 Merge pull request #393 from Kryslynn93/patch-1
Update configuration.toml
2023-10-26 07:43:00 -07:00
f965b09571 Merge pull request #398 from Codium-ai/tr/readme_updates
Update Documentation and Installation Instructions
2023-10-26 07:37:05 -07:00
e6bea76eee Typo 2023-10-26 17:07:16 +03:00
414f2b6767 Fix incremental review if there are no new commits (would have performed a full review instead) 2023-10-26 16:49:55 +03:00
6541575a0e Refactor to use pull_request synchronize event 2023-10-26 16:49:54 +03:00
02570ea797 Remove previous review comment on push event 2023-10-26 16:46:54 +03:00
b8583c998d readme 2023-10-26 12:16:58 +03:00
726594600b readme 2023-10-26 12:10:14 +03:00
c77cc1d6ed readme 2023-10-26 11:56:03 +03:00
b6c9e01a59 readme 2023-10-26 11:51:32 +03:00
ec673214c8 Update INSTALL.md 2023-10-26 11:18:07 +03:00
16777a5334 Add custom label description 2023-10-25 13:48:27 +03:00
65bb70a1dd Added support for automatic review on push event
The new feature can be enabled via the new configuration `github_app.handle_push_event`. To avoid any unwanted side-effects, the current default of this configuration is set to `false`.

The high level flow (assuming the configuration is enabled):
1. receive push event from GitHub
2. extract branch and commits from event
3. find PR url for branch (currently does not support PRs from forks)
4. perform configured commands (e.g. `/describe`, `/review -i`)

The push event flow is guarded by a backlog queue so that multiple push events on the same branch won't trigger multiple duplicate runs of the PR-Agent commands.
Example timeline:
1. push 1 - start handling event
2. push 2 - waiting to be handled while push 1 event is still running
3. push 3 - event is dropped since handling it and handling push 2 is the same, so it is redundant
4. push 1 finished being handled
5. push 2 awakens from wait and continues handling (potentially reviewing the commits of both push 2 and push 3)

All of these options are configurable and can be enabled/disabled as per the user's desire.

Additional minor changes in this PR:
1. Created `DefaultDictWithTimeout` utility class to avoid too much boilerplate code in managing caches for outdated triggers.
2. Guard against running increment review when there are no new commits.
3. Minor styling changes for incremented review text.
2023-10-25 11:15:23 +03:00
1a89c7eadf refactor + add description options 2023-10-24 22:28:57 +03:00
07617eab5a add custom labels 2023-10-24 22:06:27 +03:00
f9e4c2b098 Update configuration.toml 2023-10-23 21:34:12 -04:00
fa24413201 Custom Labels 2023-10-23 16:29:33 +03:00
b6cabda586 quick fix 2023-10-19 17:24:37 +03:00
abbce60f18 Merge remote-tracking branch 'origin/main' 2023-10-19 17:10:30 +03:00
5daaaf2c1d quick fix 2023-10-19 17:10:21 +03:00
e8f207691e Merge pull request #391 from Codium-ai/tr/readme
Update and Enhance DESCRIBE.md Documentation
2023-10-19 02:03:50 -07:00
b0dce4ceae describe 2023-10-19 12:02:12 +03:00
fc494296d7 Merge pull request #387 from Codium-ai/ok/json_logging_in_bitbucket
Enhancing Logging in Bitbucket, GitLab, and Google Cloud Storage Secret Provider
2023-10-19 11:59:26 +03:00
67b4069540 describe 2023-10-19 11:45:41 +03:00
e6defcc846 describe 2023-10-19 11:43:18 +03:00
096fcbbc17 describe 2023-10-19 11:40:01 +03:00
eb7add1c77 describe 2023-10-19 11:38:21 +03:00
1b6fb3ea53 Merge pull request #385 from Codium-ai/hl/fix_add_docs_in_scripts
Add Blacklist for Non-Editable File Extensions in Documentation
2023-10-19 11:21:36 +03:00
c57b70f1d4 Merge pull request #390 from Codium-ai/tr/readme
Enhancing Documentation and Updating Configuration for PR Descriptions
2023-10-19 01:04:24 -07:00
a2c3db463a use_bullet_points 2023-10-19 10:45:42 +03:00
193da1c356 update readme 2023-10-19 09:22:26 +03:00
5bc26880b3 update readme 2023-10-19 09:20:36 +03:00
21a1cc970e - update readme
- minor prompts change
2023-10-19 09:16:20 +03:00
954727ad67 Merge pull request #386 from Codium-ai/ok/fix_bitbucket_pipeline
Refactor Bitbucket Pipeline Integration and Update Documentation
2023-10-18 16:45:26 +03:00
1314898cbf Enhance logging in bitbucket_app, gitlab_webhook, and google_cloud_storage_secret_provider with JSON format and additional context 2023-10-18 16:44:03 +03:00
ff04d459d7 Update Bitbucket Pipeline instructions in INSTALL.md, remove redundant functionality 2023-10-18 15:46:43 +03:00
88ca501c0c Merge pull request #377 from zmeir/zmeir-review_incremental_detect_header
Get previous incremental review
2023-10-18 00:30:42 +03:00
fe284a8f91 Merge pull request #382 from Codium-ai/tr/similar_issue_fix
Enhancements and Error Handling in Similar Issue Tool
2023-10-17 09:49:35 -07:00
d41fe0cf79 comment 2023-10-17 19:45:04 +03:00
3673924fe9 Add docs editable blacklist of file extensions like sql, yaml... 2023-10-17 18:50:39 +03:00
d5c098de73 another protection 2023-10-17 10:21:05 +03:00
9f5c0daa8e protection 2023-10-17 09:43:48 +03:00
bce2262d4e Merge pull request #381 from moccajoghurt/feature-allow-custom-urls
Support Custom Domain URLs for Azure DevOps Integration
2023-10-16 22:38:27 -07:00
e6f1e0520a remove azure.com url restriction 2023-10-16 20:38:14 +02:00
d8de89ae33 Get previous incremental review
When getting the last commit in `/review -i` consider also the last __incremental__ review, not just the last __full__ review

Full disclosure I'm not really sure the `/review -i` feature work very well - I might be wrong but it seemed like the actual review in fact addressed all the changes in the PR, and not just the ones from the last review (even though it adds a link to the commit of the last review).  
I think the commit list gathered in `/review -i` doesn't propagate the actual list the reviewer uses. Again, I might be wrong, just took a brief glance at it.
2023-10-16 16:37:10 +03:00
428c38e3d9 Merge pull request #376 from Codium-ai/feature/better_logger
Refactor logging system to use custom logger across the codebase
2023-10-16 16:32:27 +03:00
7ffdf8de37 Remove verbosity level check in handle_patch_deletions test 2023-10-16 16:25:57 +03:00
83e670c5df Enhance logging context in github_app server with server type 2023-10-16 16:13:09 +03:00
c324d88be3 Refactor logging system to use custom logger across the codebase 2023-10-16 14:56:00 +03:00
41166dc271 Merge pull request #374 from Codium-ai/fix/repo_specfic_settings
Repo settings file - fix a bug and apply to GitHub app
2023-10-13 15:44:05 -07:00
e7258e732b Refactor repo-specific settings application into a utility function, fix merge bug 2023-10-14 01:39:05 +03:00
18ee9d66b0 Update Usage.md 2023-10-13 05:00:41 +03:00
8755f635b4 Update Usage.md 2023-10-10 17:23:58 +03:00
e2417ebe88 Merge pull request #363 from Codium-ai/tr/pr_suggestions
publish each suggestion seperatly only on gitlab
2023-10-08 17:01:44 +03:00
264dea2a8b Merge pull request #359 from pzarfos/codecommit_changelog
added checkmark on features table for update_changelog with CodeCommit
2023-10-08 17:00:59 +03:00
da98fd712f note 2023-10-08 16:58:22 +03:00
e6548f4fe1 simpler solution 2023-10-08 16:57:22 +03:00
e66fed2468 Merge pull request #362 from zmeir/zmeir-fix_help_message_for_bot
Fixed help message for bot user
2023-10-08 16:52:18 +03:00
1b3fb49f9c publish each suggestion seperatly only on gitlab 2023-10-08 16:50:25 +03:00
66a5f06b45 Merge pull request #360 from zmeir/zmeir-fix_typo_in_describe_md
Small typo in DESCRIBE.md
2023-10-08 16:37:51 +03:00
51c817ba29 Merge pull request #357 from jamesrom/feat/file_ignores
Add support for ignoring files
2023-10-08 16:30:02 +03:00
8f9f09ecbf Fixed help message for bot user
This changes the help message to display properly when running a custom deployment of the PR-Agent app (i.e. not via GitHub Actions, and with the setting `github_app.override_deployment_type=false`)
2023-10-08 16:19:11 +03:00
d77a71bf47 Small typo in DESCRIBE.md 2023-10-08 14:35:54 +03:00
70c0ef5ce1 added checkmark on features table for update_changelog with CodeCommit 2023-10-07 09:57:51 -04:00
92e9012fb6 Error handling 2023-10-07 09:39:53 +11:00
43dc648b05 Simplify filter 2023-10-06 22:44:29 +11:00
6dee18b24a Update usage documentation 2023-10-06 22:13:03 +11:00
baa0e95227 Code comments for ignore.toml 2023-10-06 21:53:10 +11:00
b27f57d05d Update settings, documentation 2023-10-06 21:03:36 +11:00
fd8c90041c azure 2023-10-06 08:31:31 +03:00
ea6253e2e8 revert azure 2023-10-06 08:12:11 +03:00
2a270886ea Merge pull request #354 from Codium-ai/tr/prompts
Enhancements in Markdown Conversion and Line Number Linking
2023-10-05 18:27:29 +03:00
2945c36899 source_branch 2023-10-05 18:21:52 +03:00
1bab26f1c5 gfm_supported 2023-10-05 18:08:02 +03:00
72eecbbf61 add line number 2023-10-05 17:59:08 +03:00
989c56220b add line number 2023-10-05 17:48:36 +03:00
e387086890 Add support for ignoring files
Add ignore.toml, configuration for ignoring files
Add file_filter.py, for matching files against glob/regex patterns
Update relevant code to use file filter
+Tests
2023-10-06 01:43:35 +11:00
088f256415 stable 2023-10-05 17:03:10 +03:00
d92f5284df Merge pull request #352 from Codium-ai/tr/prompts
Refactoring Patch Extra Lines and Updating Prompts
2023-10-05 08:55:14 +03:00
f3e794e50b Patch Extra Lines 2023-10-05 08:46:02 +03:00
44239f1a79 Patch Extra Lines 2023-10-05 08:38:43 +03:00
428e6382bd prompts minor update 2023-10-05 08:17:37 +03:00
d13a92515b Merge pull request #349 from Codium-ai/tr/large_prs_readme
readme updates
2023-10-02 18:20:41 +03:00
eaf7cfbcf2 readme updates 2023-10-02 18:18:15 +03:00
abb633db0f readme updates 2023-10-02 18:10:41 +03:00
ca11cfa54e Merge pull request #347 from Codium-ai/hl/document_incremental_review
Add Incremental Review to user guide
2023-10-02 14:17:04 +03:00
8df57941c6 Merge pull request #348 from Codium-ai/add-x-badge
Update README.md - add X badge
2023-10-02 13:21:49 +03:00
af45dcc7df Update README.md - add X badge 2023-10-02 11:59:56 +03:00
18c33ae6fc add screenshot 2023-10-02 11:45:05 +03:00
896d65a43a Update README.md 2023-10-02 10:59:03 +03:00
02ea2c7a40 Merge pull request #345 from brianpham93/azure-devops-get-item-error
Azure Devops: Set file content as empty string when azure_devops_client.get_item fails
2023-10-02 10:57:08 +03:00
fdbb7f176c Merge pull request #346 from Codium-ai/GadiZimerman-patch-1
Update README.md
2023-10-02 10:56:46 +03:00
5386ec359d Merge pull request #337 from Codium-ai/hl/add_docs
Add code documentations in PR
2023-10-02 10:55:36 +03:00
85f64ad895 update readme and user guide 2023-10-02 10:11:28 +03:00
589d329a3c Add error logs for troubleshooting 2023-10-02 05:37:27 +00:00
3585a4ebff Update README.md 2023-10-01 23:20:13 +03:00
706f6bf44d update Roadmap 2023-10-01 19:57:34 +03:00
54f29fcf38 add Incremental Review to user guide 2023-10-01 19:55:41 +03:00
e941fa9ec0 Add to user tools guide 2023-10-01 19:51:15 +03:00
4479c5f11b Azure Devops: Set file content as empty string when error 2023-10-01 16:22:37 +00:00
b2369c66d8 Merge commit '8d075b76ae081d0d38813f789478e4fa0f404cd8' into hl/add_docs
# Conflicts:
#	README.md
2023-10-01 13:55:50 +03:00
ab5ac8ffa8 rename vars + Add to README.md 2023-10-01 13:52:00 +03:00
ccc7f1e10a rename vars 2023-10-01 13:07:08 +03:00
8d075b76ae Merge pull request #344 from brianpham93/azure-devops-ignore-tree
Ignore change item with `gitObjectType == 'tree'`
2023-10-01 10:39:03 +03:00
32a8b0e9bc Ignore change item with gitObjectType == 'tree' 2023-10-01 07:34:01 +00:00
69c6acf89d Merge pull request #343 from Codium-ai/tr/prompts_and_readme
Update Documentation and Prompts
2023-10-01 09:05:16 +03:00
e07412c098 prompt and readme updates 2023-10-01 09:00:58 +03:00
8cec3ffde3 Merge pull request #341 from Codium-ai/tr/readme
Update and Refactor Documentation
2023-09-30 10:54:58 +03:00
25bc54785b update docs 2023-09-30 10:51:37 +03:00
8b033ccc94 Merge pull request #340 from Codium-ai/tr/readme
Update Documentation and CLI Execution Method
2023-09-29 16:43:10 +03:00
2003b6915b typos 2023-09-29 16:39:25 +03:00
175218c779 cli 2023-09-29 16:31:13 +03:00
f96c6cbc95 Merge pull request #339 from Codium-ai/tr/readme
Enhancements to PR-Agent Documentation and Settings
2023-09-29 11:56:02 +03:00
4dbb70c9d5 typos 2023-09-29 11:54:37 +03:00
73cc7c2d71 typos 2023-09-29 11:53:39 +03:00
902975b660 Merge remote-tracking branch 'origin/tr/readme' into tr/readme 2023-09-29 11:44:19 +03:00
813fa8571e tools guide 2023-09-29 11:44:03 +03:00
b5c505f727 ASK 2023-09-29 11:43:38 +03:00
26b9e8a235 ASK 2023-09-29 11:37:31 +03:00
c90e72cb75 Improve 2023-09-29 11:30:47 +03:00
55f022d93e improve 2023-09-29 11:24:26 +03:00
71b532d4d5 <kbd> 2023-09-29 10:38:01 +03:00
c18ae77299 <kbd> 2023-09-29 10:36:58 +03:00
2f0fa246c0 borders 2023-09-29 10:34:18 +03:00
a371f7ab84 review 2023-09-29 10:30:20 +03:00
e1149862b2 similar issue 2023-09-29 10:01:32 +03:00
b8f93516ce describe 2023-09-29 09:47:13 +03:00
d15d374cdc Merge pull request #338 from coka-stefan/main
Add lockb to bad extensions
2023-09-29 09:20:49 +03:00
cae0f627e2 Add lockb to bad extensions 2023-09-28 22:26:25 +02:00
7fbdc3aead rstrip 2023-09-28 22:47:26 +03:00
0551922839 Merge commit '663ae92bdf3bb3a22b8b7ab437385c882f96e475' into hl/add_docs
# Conflicts:
#	pr_agent/tools/pr_add_docs.py
2023-09-28 22:46:07 +03:00
043d453cab add doc placement before after 2023-09-28 22:44:15 +03:00
663ae92bdf Add Docs 2023-09-28 22:42:03 +03:00
96824aa9e2 Revert "Add Docs"
This reverts commit 5cca299b16.
2023-09-28 21:16:14 +03:00
5cca299b16 Add Docs 2023-09-28 21:13:48 +03:00
cd3527f7d4 add configurable docstring style 2023-09-28 20:58:37 +03:00
bb12c75431 reformat 2023-09-28 20:15:18 +03:00
4accddcaa7 revert verbosity 2023-09-28 20:12:36 +03:00
bb8a0f10f4 refine /add_docs 2023-09-28 20:11:18 +03:00
c3cbaaf09e Initial add docs 2023-09-27 16:48:17 +03:00
a6e65e867f release notes v0.8 2023-09-27 09:23:19 +03:00
0df8071673 Merge pull request #331 from dulalbert/patch-2
INSTALL.md Fixed typo in the hyperlink
2023-09-26 08:22:07 +03:00
b17a4d9551 INSTALL.md Fixed typo in the hyperlink 2023-09-26 10:29:26 +08:00
dc7db4cbdd Merge pull request #330 from Codium-ai/tr/action_defaults
Enhancements in GitHub Action and Error Handling
2023-09-25 18:59:57 +03:00
4e94fcc372 typo 2023-09-25 18:57:50 +03:00
4c72cfbff4 auto tools in github action 2023-09-25 18:56:10 +03:00
ac89867ac7 Merge pull request #329 from Codium-ai/tr/action_defaults
Adding Environment Variables for Automated Actions in GitHub
2023-09-25 18:30:53 +03:00
34ed598c20 yaml 2023-09-25 18:30:20 +03:00
e7aee84ea8 isinstance 2023-09-25 18:23:56 +03:00
388684e2e8 none 2023-09-25 18:19:35 +03:00
8f81c18647 auto commands in github action 2023-09-25 18:01:32 +03:00
ba78475944 Merge pull request #327 from Codium-ai/tr/no_comments
Update Review Guidelines to Exclude Comments
2023-09-25 17:25:31 +03:00
75dd5688fa Merge pull request #324 from pzarfos/feature/get_pr_id_320
Added 'get_pr_id' function for the CodeCommit provider
2023-09-25 17:24:21 +03:00
aa32024078 no comments 2023-09-25 16:58:08 +03:00
9167c20512 added unit tests 2023-09-23 10:41:53 -04:00
a7fb5d98b1 add get_pr_id() to CodeCommitProvider 2023-09-23 08:08:46 -04:00
fda47bb5cf Merge pull request #304 from sarbjitsinghgrewal/fix_bitbucket_pipeline
Fix bitbucket pipeline
2023-09-22 14:36:42 +03:00
9c4f849066 Merge pull request #321 from dulalbert/patch-1
Correct typo in pr_reviewer_prompts.toml
2023-09-22 12:44:09 +03:00
3e2e2d6c6e update install.md 2023-09-22 12:23:41 +05:30
56cc804fcf Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_pipeline 2023-09-22 12:19:49 +05:30
62746294e3 Correct typo in pr_reviewer_prompts.toml 2023-09-22 10:23:01 +08:00
d384b0644e Merge pull request #320 from Codium-ai/tr/pr_id
Implementing get_pr_id method in Git Providers
2023-09-21 21:38:49 +03:00
3e07fe618f pr_id gitlab 2023-09-21 21:35:00 +03:00
be54fb5bf8 pr_id 2023-09-21 21:29:41 +03:00
46ec3c0754 implement suggestions in bitbucket pipeline 2023-09-21 14:42:59 +05:30
5e608cc7e7 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_pipeline 2023-09-21 12:48:32 +05:30
04162564ca Merge pull request #315 from Codium-ai/tr/fixes_20_9
Fixing potential null values in git patch processing
2023-09-20 17:00:59 +03:00
992f51a019 protections 2023-09-20 15:59:35 +03:00
2bc25b7435 Merge remote-tracking branch 'origin/main' into tr/fixes_20_9 2023-09-20 15:58:05 +03:00
fcd9821d10 protections 2023-09-20 15:57:06 +03:00
911ad299e2 Merge pull request #314 from Codium-ai/mrT23-patch-1
Update README.md
2023-09-20 14:50:53 +03:00
fbfa186733 Update README.md 2023-09-20 14:50:21 +03:00
7545b25823 Update README.md 2023-09-20 14:49:50 +03:00
1370a051f1 Update INSTALL.md 2023-09-20 14:47:52 +03:00
dcbd3132d1 Release notes 2023-09-20 14:41:24 +03:00
497f84b3bd Update release notes 2023-09-20 14:23:55 +03:00
c2fe2fc657 Added a release notes file 2023-09-20 13:50:10 +03:00
f7abdc6ae8 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_pipeline 2023-09-20 14:47:21 +05:30
d327245edf Merge pull request #312 from Codium-ai/tr/fixes_20_9
Enhancing error handling in PR review tools
2023-09-20 07:59:02 +03:00
632de3f186 protections 2023-09-20 07:39:56 +03:00
de14b0e4c0 Merge pull request #310 from never-known-soldier/update/README.md
Update README.md file
2023-09-19 13:29:35 +03:00
f010d1389b Merge branch 'main' into update/README.md 2023-09-19 13:28:39 +03:00
4411f6d88a Update README.md file 2023-09-19 12:54:33 +05:30
a2ca43afcd Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_pipeline 2023-09-18 10:34:18 +05:30
1f62520606 Merge pull request #306 from Codium-ai/tr/etr
Adding Estimated Review Effort Feature and Handling Cases with No Detected Language
2023-09-17 17:10:52 +03:00
c0511c954e icon 2023-09-17 17:08:02 +03:00
818ab5a9e8 fixed tests 2023-09-17 16:56:23 +03:00
291ffdd6ae gfm_markdown 2023-09-17 16:51:16 +03:00
4fbe7d14b5 protection for no language 2023-09-17 16:41:53 +03:00
ea91a38541 Estimated effort to review 2023-09-17 16:31:58 +03:00
caaee4e43d Estimated time to review 2023-09-15 17:09:58 +03:00
43af4aa182 remove token from config 2023-09-15 16:08:13 +05:30
e343ce8468 bitbucket pipeline for adding reviews 2023-09-15 16:05:55 +05:30
7b2c01181b Merge pull request #3 from sarbjitsinghgrewal/fix_bitbucket_pipeline
Fix bitbucket pipeline
2023-09-15 12:44:14 +05:30
978c56c128 update pipeline 2023-09-15 12:38:34 +05:30
4043dfff9e Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_pipeline 2023-09-14 15:31:18 +05:30
279d45996f Merge pull request #301 from Codium-ai/add-links-to-readme
Addition of Relevant Links to README.md
2023-09-14 12:38:57 +03:00
01aa038ad6 Update README.md - add links 2023-09-14 11:53:47 +03:00
084256b923 fixed config 2023-09-14 08:23:34 +03:00
dc42713217 Merge pull request #285 from Codium-ai/markers
Markers
2023-09-14 08:21:35 +03:00
99f17666c5 merge 2023-09-14 08:20:36 +03:00
bba22667f1 merge 2023-09-14 08:13:00 +03:00
1b8349b0ef merge 2023-09-14 07:47:04 +03:00
b94e3521d1 Merge remote-tracking branch 'origin/main' into markers
# Conflicts:
#	pr_agent/tools/pr_description.py
2023-09-14 07:46:30 +03:00
32931f0bc0 Update Usage.md 2023-09-13 10:38:31 +03:00
72ac8e8091 Merge pull request #299 from Codium-ai/tr/readme
Enhancement of Similar Issue Tool and Documentation Updates
2023-09-13 08:19:13 +03:00
33045e6898 graphic adjustments 2023-09-13 08:17:13 +03:00
069c3a8e5c Merge remote-tracking branch 'origin/tr/readme' into tr/readme 2023-09-13 08:16:36 +03:00
9c0656c296 graphic adjustments 2023-09-13 08:16:23 +03:00
228ee26541 graphic adjustments 2023-09-13 08:16:08 +03:00
f8d548367f graphic adjustments 2023-09-13 08:15:15 +03:00
d3f466f59b graphic adjustments 2023-09-13 08:04:36 +03:00
6b45940128 Merge remote-tracking branch 'origin/main' into tr/readme 2023-09-13 07:47:21 +03:00
a52e94fcbc similar issue 2023-09-13 07:46:43 +03:00
ee3874f0aa Merge pull request #297 from Codium-ai/tr/fix_tests
Enhancing Logging in pr_similar_issue.py
2023-09-13 07:27:50 +03:00
31ba7acf49 Support issue comments in GitHub Actions 2023-09-12 16:53:54 +03:00
b7a2551cab Support issue comments in GitHub Actions 2023-09-12 16:46:02 +03:00
d4eb100cbc Support issue comments in GitHub Actions 2023-09-12 16:44:20 +03:00
21feb92b75 Support issue comments in GitHub Actions 2023-09-12 16:41:12 +03:00
2f6178306f Fix a bug in GitHub Actions 2023-09-12 13:28:35 +03:00
36e7c1c22f Merge remote-tracking branch 'origin/main' 2023-09-12 13:24:55 +03:00
c31baa5aea Fix a bug in GitHub Actions 2023-09-12 13:24:47 +03:00
67052aa714 add bitbucket access token 2023-09-12 11:28:36 +05:30
caee7cbf50 add bitbucket access token 2023-09-12 11:20:38 +05:30
9bee3055c2 add bitbucket access token 2023-09-12 11:15:10 +05:30
901eda2f10 logs 2023-09-12 07:57:21 +03:00
8cf7d2d0b1 Merge pull request #296 from Codium-ai/tr/fix_tests
gfm_supported
2023-09-12 07:49:19 +03:00
d7f43d6ee0 gfm_supported 2023-09-12 07:43:15 +03:00
9bd5140ea4 update access token 2023-09-11 16:33:45 +05:30
12bd9e8b42 add bitbucket pipeline 2023-09-11 16:08:23 +05:30
ca8997b616 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_publish_description 2023-09-11 09:36:23 +05:30
8e42162b5e Merge pull request #278 from sarbjitsinghgrewal/fix_bitbucket_publish_description
Fix bitbucket publish description
2023-09-10 14:13:26 +03:00
98d0835c48 Merge remote-tracking branch 'origin/main' into fix_bitbucket_publish_description 2023-09-10 14:08:17 +03:00
2aef9dfe55 Merge remote-tracking branch 'origin/main' into fix_bitbucket_publish_description 2023-09-10 14:06:54 +03:00
115b513c9b Remove 'bitbucket' explicit dependency anywhere that's not in bitbucket_provider.py 2023-09-10 14:06:13 +03:00
fd63fe4c95 Merge pull request #293 from Codium-ai/tr/litellm_debugger
Integration of Litellm Client with AI Handler
2023-09-10 13:56:06 +03:00
d40285e4d3 Merge branch 'main' into tr/litellm_debugger 2023-09-10 13:40:35 +03:00
517658fb37 Merge pull request #282 from Codium-ai/tr/issue_tool
Adding Similar Issue Tool and Pinecone Integration
2023-09-10 13:39:34 +03:00
f9f0f220c2 pinecone-datasets 2023-09-10 13:31:36 +03:00
6382b8a68b LITELLM_TOKEN 2023-09-10 13:28:56 +03:00
e371b217ec Merge remote-tracking branch 'origin/main' into tr/litellm_debugger 2023-09-10 13:27:19 +03:00
7dec7b0583 Merge pull request #291 from krrishdholakia/main
adding documentation on how to call local hf models
2023-09-10 13:25:44 +03:00
bf6a235add pinecone-datasets==0.6.1 2023-09-10 13:16:05 +03:00
1d9489c734 Merge remote-tracking branch 'origin/tr/issue_tool' into tr/issue_tool 2023-09-10 08:39:20 +03:00
bd588b4509 solved dependencies 2023-09-10 08:39:03 +03:00
245f29e58a solved dependencies 2023-09-10 08:22:42 +03:00
7f5f2d2d1a solved dependencies 2023-09-10 08:07:39 +03:00
fe500845b7 upgrade pip 2023-09-10 07:46:51 +03:00
b42b2536b5 upgrade pip 2023-09-10 07:39:01 +03:00
498ad3d19c upgrade pip 2023-09-10 07:36:25 +03:00
892dbe458e litellm client 2023-09-09 17:35:45 +03:00
1b098aea13 adding documentation on how to call local hf models 2023-09-08 09:59:44 -07:00
ed1816a2d7 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_publish_description 2023-09-08 11:24:05 +05:30
e90c9e5853 Merge pull request #287 from cloudlinux/gerrit
[gerrit] Added support project's config file: `.pr_agent.toml`
2023-09-07 19:06:32 +03:00
e4f28b157f Added support project's config file: .pr_agent.toml
+ removed markdown/html formatting from the review due to gerrit does not support it
2023-09-07 13:13:07 +01:00
6fb8a882af ordering requirements.txt 2023-09-07 12:41:31 +03:00
9889d26d3e merged main 2023-09-07 12:31:22 +03:00
b23a4c0535 Merge remote-tracking branch 'origin/main' into tr/issue_tool
# Conflicts:
#	requirements.txt
2023-09-07 12:30:16 +03:00
0f7a481eaa Merge pull request #277 from krrishdholakia/hf-usage
Showing how to use huggingface models with PR-Agent
2023-09-07 12:28:47 +03:00
3fc88b2bc4 Merge pull request #276 from krrishdholakia/main
Add docs on using Azure
2023-09-07 12:20:26 +03:00
ed5aaaab45 Merge branch 'main' into main 2023-09-07 12:19:59 +03:00
145b5db458 added 'publish_description_as_comment' support 2023-09-07 12:10:33 +03:00
8321792a8d == 2023-09-06 18:12:16 +03:00
8af8fd8e5d github action 2023-09-06 17:43:43 +03:00
753ea3e44c Update INSTALL.md 2023-09-06 11:35:41 +03:00
660601f7c5 Merge pull request #280 from Codium-ai/coditamar/install
Update INSTALL.md
2023-09-06 11:32:18 +03:00
4e7f67f596 Merge pull request #279 from Codium-ai/coditamar/readme-minor-enhancement
Enhancements to README.md for Improved Clarity and Detail
2023-09-06 11:31:55 +03:00
e486addb8f Update INSTALL.md
minor clarification to Method 8 GitLab
2023-09-06 11:28:19 +03:00
4a5310e2a1 Update README.md 2023-09-06 10:46:25 +03:00
8962c9cf8a stable 2023-09-06 09:43:23 +03:00
bc95cf5b8e stable 2023-09-06 09:12:25 +03:00
dcd8196b94 Merge remote-tracking branch 'origin/main' into tr/issue_tool
# Conflicts:
#	pr_agent/settings/configuration.toml
2023-09-06 08:43:41 +03:00
901c1dc3f0 issue tool 2023-09-06 08:43:01 +03:00
adb9964823 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_publish_description 2023-09-06 09:32:43 +05:30
335877c4a7 fix publish description for bitbucket 2023-09-06 09:26:23 +05:30
5da6a0147c showing how to use huggingface models 2023-09-05 16:23:22 -07:00
cd1ae55f4f bump litellm version to fix azure deployment id error 2023-09-05 15:26:45 -07:00
ca50724952 adding details on calling azure 2023-09-05 15:19:56 -07:00
460b315b53 Add Gitlab webhook installation documentation 2023-09-05 18:34:19 +03:00
00ff516e8a Merge pull request #264 from Codium-ai/ok/gitlab_webhook
Implementing Gitlab Webhook Secret Verification
2023-09-05 18:33:03 +03:00
55b3c3fe5c Add Gitlab webhook installation documentation 2023-09-05 18:31:29 +03:00
1443df7227 Merge remote-tracking branch 'origin/main' into ok/gitlab_webhook 2023-09-05 18:15:51 +03:00
739b63f73b Merge pull request #272 from cloudlinux/gerrit
Gerrit support
2023-09-05 13:54:57 +03:00
4a54532b6a Set github provider by default
- merge README.md
2023-09-05 09:09:35 +01:00
0dbe64e401 Merge branch 'main' into gerrit 2023-09-05 09:04:32 +01:00
c0b23e1091 Merge remote-tracking branch 'origin/main' into tr/issue_tool
# Conflicts:
#	pr_agent/algo/utils.py
2023-09-05 08:05:33 +03:00
704c169181 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_improve_issue 2023-09-05 10:00:07 +05:30
746140b26e Add support for markers in description 2023-09-04 12:11:39 -04:00
53ce609266 Msg is mandatory field for the request 2023-09-04 15:50:36 +01:00
7584ec84ce Update README.md 2023-09-04 15:48:18 +01:00
140760c517 Add Gitlab webhook secret 2023-09-04 16:39:31 +03:00
56e9493f7a Add Gitlab webhook secret 2023-09-04 15:29:21 +03:00
958ecf333a Merge pull request #271 from pzarfos/codecommit_implemented_improve_cli
implement 'improve' command for CodeCommit
2023-09-04 07:56:23 +03:00
ae3d7067d3 implemented 'improve' command for CodeCommit 2023-09-03 09:22:08 -04:00
a49e81d959 Merge pull request #270 from Codium-ai/tr/readme_updates
Tr/readme updates
2023-09-03 11:43:23 +03:00
916d7c236e update README.md 2023-09-03 11:42:11 +03:00
6343d35616 Merge pull request #269 from pzarfos/remove_extra_swift
Removed duplicate swift statement
2023-09-03 10:35:27 +03:00
0203086aac removed duplicate swift statement 2023-09-02 15:39:57 -04:00
0066156aca Merge pull request #268 from Codium-ai/tr/readme_updates
Update Documentation and Reorder Installation Methods
2023-09-02 09:44:56 +03:00
544bac7010 update README.md 2023-09-02 09:43:46 +03:00
34090b078b Merge remote-tracking branch 'origin/main' into tr/readme_updates 2023-09-02 09:37:57 +03:00
9567199bb2 update README.md 2023-09-02 09:37:44 +03:00
1f7a833a54 update README.md 2023-09-02 09:33:33 +03:00
990f69a95d update README.md 2023-09-02 09:25:38 +03:00
2b8a8ce824 update README.md 2023-09-02 09:19:35 +03:00
6585854c85 Merge pull request #267 from Codium-ai/tr/readme_updates
Update Documentation and README
2023-09-01 20:28:09 +03:00
98019fe97f update CONFIGURATION.md 2023-09-01 20:27:29 +03:00
d52c11b907 update CONFIGURATION.md 2023-09-01 20:26:36 +03:00
e79bcbed93 update CONFIGURATION.md 2023-09-01 20:25:07 +03:00
690c819479 update CONFIGURATION.md 2023-09-01 20:22:10 +03:00
630d1d9e03 Merge remote-tracking branch 'origin/tr/readme_updates' into tr/readme_updates
# Conflicts:
#	CONFIGURATION.md
2023-09-01 20:14:56 +03:00
20c32375e1 auto 2023-09-01 20:14:14 +03:00
44b790567b update readme 2023-09-01 20:10:23 +03:00
4d6d6c4812 update 2023-09-01 20:09:53 +03:00
7f6493009c update 2023-09-01 20:05:33 +03:00
7a6efbcb55 update 2023-09-01 19:56:20 +03:00
777c773a90 update 2023-09-01 19:50:10 +03:00
f7c698ff54 update 2023-09-01 19:40:38 +03:00
1b780c0496 Merge pull request #265 from szecsip/feature_azure_devops_docs
Add docs and dependencies for Azure DevOps provider
2023-09-01 19:32:23 +03:00
2e095807b7 Update README.md
Added information about features of gerrit provider
2023-09-01 15:05:48 +01:00
ae98cfe17b Return status of upload suggestion to the gerrit 2023-09-01 14:56:16 +01:00
35a6eb2e52 Add unique patch to according to multiple execution of publish_code_suggestions 2023-09-01 14:51:29 +01:00
8b477c694c Fix after rebase on main branch 2023-09-01 14:41:56 +01:00
1254ad1727 Gerrit:
- support reflect/answer
2023-09-01 14:31:35 +01:00
eeea38dab3 Gerrit support 2023-09-01 14:31:34 +01:00
8983fd9071 Merge pull request #261 from Codium-ai/tr/github_app
Update Configuration Documentation
2023-08-31 20:43:58 +03:00
918ae25654 Merge remote-tracking branch 'origin/main' into tr/github_app 2023-08-31 20:36:56 +03:00
de39595522 Merge pull request #252 from zmeir/zmeir-try_to_fix_split_line_ranges
Try to refine the prompt to prevent split line ranges that give bad suggestions
2023-08-31 20:36:33 +03:00
4c6595148b Add Gitlab webhook secret 2023-08-31 17:03:58 +03:00
02e0f958e7 Add Gitlab webhook secret 2023-08-31 14:56:45 +03:00
be19b64542 add dependencies for Azure DevOps provider 2023-08-31 11:53:54 +00:00
24900305d6 update docs for Azure DevOps 2023-08-31 11:50:41 +00:00
06d00032df update docs for Azure DevOps 2023-08-31 11:47:51 +00:00
244cbbd27f Merge pull request #262 from idavidov/idavidov/add_reference
Idavidov/add reference
2023-08-31 13:24:59 +03:00
970a7896e9 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_improve_issue 2023-08-31 13:35:32 +05:30
8263bf5f9c small refactor of azure devops 2023-08-31 09:26:16 +03:00
8823d8c0e9 small format change 2023-08-31 09:21:40 +03:00
5cbcef276c cross link in INSTALL for GitAPP configuration overwrite 2023-08-31 09:19:02 +03:00
ce9014073c Editing the prompts 2023-08-31 08:19:33 +03:00
376c4523dd Editing the prompts 2023-08-31 08:08:09 +03:00
e0ca594a69 Editing the prompts 2023-08-31 08:06:43 +03:00
48233fde23 Editing the prompts 2023-08-31 08:02:14 +03:00
9c05a6b1b5 Merge pull request #258 from Codium-ai/tr/github_app
Update GitHub App Configuration Instructions
2023-08-31 02:17:36 +03:00
da848d7e39 Merge pull request #260 from zmeir/zmeir-fix_describe_user_description
Fix #254
2023-08-31 02:16:55 +03:00
c6c97ac98a Try to change the improve command prompt to prevent split lines range 2023-08-30 23:33:38 +03:00
92e23ff260 Fix #254 2023-08-30 23:05:41 +03:00
aa03654ffc Merge pull request #259 from krrishdholakia/patch-3
bump litellm version - prevent default logging
2023-08-30 20:32:59 +03:00
85130c0d30 Merge pull request #255 from pzarfos/issue_138_codecommit_describe
Enhancement of AWS CodeCommit support in PR-Agent
2023-08-30 20:30:50 +03:00
3c27432f50 GitHub App instructions 2023-08-30 19:53:28 +03:00
eec62c14dc bump litellm version - prevent default logging 2023-08-30 09:49:01 -07:00
ad6dd38fe3 GitHub App instructions 2023-08-30 19:46:33 +03:00
307b3b4bf7 GitHub App instructions 2023-08-30 19:42:46 +03:00
8e7e13ab62 Merge pull request #253 from Codium-ai/coditamar/readme-minor
Enhancements to README.md
2023-08-30 18:45:59 +03:00
bd085e610a Merge pull request #256 from Codium-ai/ok/override_settings
Allow overriding GitHub app default action by using repo local file
2023-08-30 18:44:45 +03:00
f26264daf1 added describe command to CodeCommit 2023-08-29 17:59:52 -04:00
2aaa722102 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_improve_issue 2023-08-29 09:49:19 +05:30
edaeb99b43 Update README.md
type

Co-authored-by: Tim Perkins <tjwp@users.noreply.github.com>
2023-08-28 22:29:08 +03:00
ce54a7b79e Update README.md 2023-08-28 22:08:08 +03:00
39522abc03 fix conflicts 2023-08-28 11:21:47 +05:30
0e42634da4 add publish_labels and get_labels functions 2023-08-25 10:15:30 +05:30
f0dc485305 Merge branch 'main' of https://github.com/Codium-ai/pr-agent into fix_bitbucket_improve_issue 2023-08-24 16:14:29 +05:30
db6bf41051 update readme 2023-08-24 15:56:20 +05:30
67ff50583a fix improve, update_changelog and review inline comment 2023-08-24 11:52:20 +05:30
6693aa3cbc semi stable 2023-08-20 15:01:06 +03:00
93 changed files with 5895 additions and 1189 deletions

View File

@ -24,4 +24,9 @@ jobs:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
OPENAI_ORG: ${{ secrets.OPENAI_ORG }} # optional OPENAI_ORG: ${{ secrets.OPENAI_ORG }} # optional
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
GITHUB_ACTION.AUTO_REVIEW: true
GITHUB_ACTION.AUTO_IMPROVE: true

6
.pr_agent.toml Normal file
View File

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

View File

@ -1,57 +0,0 @@
## Configuration
The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**
### Working from CLI
When running from source (CLI), your local configuration file will be initially used.
Example for invoking the 'review' tools via the CLI:
```
python cli.py --pr-url=<pr_url> review
```
In addition to general configurations, the 'review' tool will use parameters from the `[pr_reviewer]` section (every tool has a dedicated section in the configuration file).
Note that you can print results locally, without publishing them, by setting in `configuration.toml`:
```
[config]
publish_output=true
verbosity_level=2
```
This is useful for debugging or experimenting with the different tools.
### Working from pre-built repo (GitHub Action/GitHub App/Docker)
When running PR-Agent from a pre-built repo, the default configuration file will be loaded.
To edit the configuration, you have two options:
1. Place a local configuration file in the root of your local repo. The local file will be used instead of the default one.
2. For online usage, just add `--config_path=<value>` to you command, to edit a specific configuration value.
For example if you want to edit `pr_reviewer` configurations, you can run:
```
/review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false ...
```
Any configuration value in `configuration.toml` file can be similarly edited.
### General configuration parameters
#### Changing a model
See [here](pr_agent/algo/__init__.py) for the list of available models.
To use Llama2 model, for example, set:
```
[config]
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
[replicate]
key = ...
```
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
#### Extra instructions
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
```
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
```

View File

@ -2,7 +2,8 @@ FROM python:3.10 as base
WORKDIR /app WORKDIR /app
ADD pyproject.toml . ADD pyproject.toml .
RUN pip install . && rm pyproject.toml ADD requirements.txt .
RUN pip install . && rm pyproject.toml requirements.txt
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
ADD pr_agent pr_agent ADD pr_agent pr_agent
ADD github_action/entrypoint.sh / ADD github_action/entrypoint.sh /

View File

@ -4,128 +4,74 @@
To get started with PR-Agent quickly, you first need to acquire two tokens: To get started with PR-Agent quickly, you first need to acquire two tokens:
1. An OpenAI key from [here](https://platform.openai.com/), with access to GPT-4. 1. An OpenAI key from [here](https://platform.openai.com/), with access to GPT-4.
2. A GitHub personal access token (classic) with the repo scope. 2. A GitHub\GitLab\BitBucket personal access token (classic) with the repo scope.
There are several ways to use PR-Agent: There are several ways to use PR-Agent:
- [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required) **Locally**
- [Method 2: Run as a GitHub Action](INSTALL.md#method-2-run-as-a-github-action) - [Using Docker image (no installation required)](INSTALL.md#use-docker-image-no-installation-required)
- [Method 3: Run from source](INSTALL.md#method-3-run-from-source) - [Run from source](INSTALL.md#run-from-source)
- [Method 4: Run as a polling server](INSTALL.md#method-4-run-as-a-polling-server)
- [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app) **GitHub specific methods**
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function) - [Run as a GitHub Action](INSTALL.md#run-as-a-github-action)
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup) - [Run as a polling server](INSTALL.md#run-as-a-polling-server)
- [Run as a GitHub App](INSTALL.md#run-as-a-github-app)
- [Deploy as a Lambda Function](INSTALL.md#deploy-as-a-lambda-function)
- [AWS CodeCommit](INSTALL.md#aws-codecommit-setup)
**GitLab specific methods**
- [Run a GitLab webhook server](INSTALL.md#run-a-gitlab-webhook-server)
**BitBucket specific methods**
- [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)
- [Bitbucket server and data center](INSTALL.md#bitbucket-server-and-data-center)
--- ---
### Method 1: Use Docker image (no installation required) ### Use Docker image (no installation required)
To request a review for a PR, or ask a question about a PR, you can run directly from the Docker image. Here's how: A list of the relevant tools can be found in the [tools guide](./docs/TOOLS_GUIDE.md).
1. To request a review for a PR, run the following command: To invoke a tool (for example `review`), you can run directly from the Docker image. Here's how:
- For GitHub:
``` ```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> review docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent:latest --pr_url <pr_url> review
``` ```
2. To ask a question about a PR, run the following command: - For GitLab:
``` ```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>" docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> codiumai/pr-agent:latest --pr_url <pr_url> review
``` ```
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest.
The digest is a unique identifier for a specific version of an image. You can pull and run an image using its digest by referencing it like so: repository@sha256:digest. Always ensure you're using the correct and trusted digest for your operations.
1. To request a review for a PR using a specific digest, run the following command: Note: If you have a dedicated GitLab instance, you need to specify the custom url as variable:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> GITLAB.URL=<your gitlab instance url> codiumai/pr-agent:latest --pr_url <pr_url> review
```
- For BitBucket:
```
docker run --rm -it -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=<pr_url> review
```
For other git providers, update CONFIG.GIT_PROVIDER accordingly, and check the `pr_agent/settings/.secrets_template.toml` file for the environment variables expected names and values.
---
If you want to ensure you're running a specific version of the Docker image, consider using the image's digest:
```bash ```bash
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
``` ```
2. To ask a question about a PR using the same digest, run the following command: Or you can run a [specific released versions](./RELEASE_NOTES.md) of pr-agent, for example:
```bash
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> ask "<your question>"
``` ```
codiumai/pr-agent@v0.9
Possible questions you can ask include:
- What is the main theme of this PR?
- Is the PR ready for merge?
- What are the main changes in this PR?
- Should this PR be split into smaller parts?
- Can you compose a rhymed song about this PR?
---
### Method 2: Run as a GitHub Action
You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action.
1. Add the following file to your repository under `.github/workflows/pr_agent.yml`:
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
** if you want to pin your action to a specific commit for stability reasons
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@<commit_sha>
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
2. Add the following secret to your repository under `Settings > Secrets`:
```
OPENAI_KEY: <your key>
```
The GITHUB_TOKEN secret is automatically created by GitHub.
3. Merge this change to your main branch.
When you open your next PR, you should see a comment from `github-actions` bot with a review of your PR, and instructions on how to use the rest of the tools.
4. You may configure PR-Agent by adding environment variables under the env section corresponding to any configurable property in the [configuration](./CONFIGURATION.md) file. Some examples:
```yaml
env:
# ... previous environment values
OPENAI.ORG: "<Your organization name under your OpenAI account>"
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review
PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions
``` ```
--- ---
### Method 3: Run from source ### Run from source
1. Clone this repository: 1. Clone this repository:
@ -151,18 +97,94 @@ chmod 600 pr_agent/settings/.secrets.toml
``` ```
export PYTHONPATH=[$PYTHONPATH:]<PATH to pr_agent folder> export PYTHONPATH=[$PYTHONPATH:]<PATH to pr_agent folder>
python pr_agent/cli.py --pr_url <pr_url> review python3 -m pr_agent.cli --pr_url <pr_url> review
python pr_agent/cli.py --pr_url <pr_url> ask <your question> python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
python pr_agent/cli.py --pr_url <pr_url> describe python3 -m pr_agent.cli --pr_url <pr_url> describe
python pr_agent/cli.py --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> generate_labels
python3 -m pr_agent.cli --issue_url <issue_url> similar_issue
...
``` ```
--- ---
### Method 4: Run as a polling server ### Run as a GitHub Action
Request reviews by tagging your Github user on a PR
You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action.
1. Add the following file to your repository under `.github/workflows/pr_agent.yml`:
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
** if you want to pin your action to a specific release (v0.7 for example) for stability reasons, use:
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@v0.7
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
2. Add the following secret to your repository under `Settings > Secrets and variables > Actions > New repository secret > Add secret`:
```
Name = OPENAI_KEY
Secret = <your key>
```
The GITHUB_TOKEN secret is automatically created by GitHub.
3. Merge this change to your main branch.
When you open your next PR, you should see a comment from `github-actions` bot with a review of your PR, and instructions on how to use the rest of the tools.
4. You may configure PR-Agent by adding environment variables under the env section corresponding to any configurable property in the [configuration](pr_agent/settings/configuration.toml) file. Some examples:
```yaml
env:
# ... previous environment values
OPENAI.ORG: "<Your organization name under your OpenAI account>"
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review
PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions
```
---
### Run as a polling server
Request reviews by tagging your GitHub user on a PR
Follow [steps 1-3](#run-as-a-github-action) of the GitHub Action setup.
Follow steps 1-3 of method 2.
Run the following command to start the server: Run the following command to start the server:
``` ```
@ -171,7 +193,7 @@ python pr_agent/servers/github_polling.py
--- ---
### Method 5: Run as a GitHub App ### Run as a GitHub App
Allowing you to automate the review process on your private or public repositories. Allowing you to automate the review process on your private or public repositories.
1. Create a GitHub App from the [Github Developer Portal](https://docs.github.com/en/developers/apps/creating-a-github-app). 1. Create a GitHub App from the [Github Developer Portal](https://docs.github.com/en/developers/apps/creating-a-github-app).
@ -184,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:
@ -251,11 +274,14 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories. 9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
> **Note:** When running PR-Agent from GitHub App, the default configuration file (configuration.toml) will be loaded.<br>
> However, you can override the default tool parameters by uploading a local configuration file `.pr_agent.toml`<br>
> For more information please check out the [USAGE GUIDE](./Usage.md#working-with-github-app)
--- ---
### Method 6 - Deploy as a Lambda Function ### Deploy as a Lambda Function
1. Follow steps 1-5 of [Method 5](#method-5-run-as-a-github-app). 1. Follow steps 1-5 of [Method 5](#run-as-a-github-app).
2. Build a docker image that can be used as a lambda function 2. Build a docker image that can be used as a lambda function
```shell ```shell
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
@ -267,12 +293,13 @@ 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](#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`
--- ---
### Method 7 - AWS CodeCommit Setup ### AWS CodeCommit Setup
Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the pr-agent CLI on the command line, using AWS credentials stored in environment variables. (More features will be added in the future.) The following is a set of instructions to have pr-agent do a review of your CodeCommit pull request from the command line: Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the pr-agent CLI on the command line, using AWS credentials stored in environment variables. (More features will be added in the future.) The following is a set of instructions to have pr-agent do a review of your CodeCommit pull request from the command line:
@ -309,7 +336,9 @@ Example IAM permissions to that user to allow access to CodeCommit:
"codecommit:Get*", "codecommit:Get*",
"codecommit:List*", "codecommit:List*",
"codecommit:PostComment*", "codecommit:PostComment*",
"codecommit:PutCommentReaction" "codecommit:PutCommentReaction",
"codecommit:UpdatePullRequestDescription",
"codecommit:UpdatePullRequestTitle"
], ],
"Resource": "*" "Resource": "*"
} }
@ -338,9 +367,96 @@ PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
review review
``` ```
### Appendix - **Debugging LLM API Calls** ---
If you're testing your codium/pr-agent server, and need to see if calls were made successfully + the exact call logs, you can use the [LiteLLM Debugger tool](https://docs.litellm.ai/docs/debugging/hosted_debugging).
You can do this by setting `litellm_debugger=true` in configuration.toml. Your Logs will be viewable in real-time @ `admin.litellm.ai/<your_email>`. Set your email in the `.secrets.toml` under 'user_email'. ### Run a GitLab webhook server
<img src="./pics/debugger.png" width="800"/> 1. From the GitLab workspace or group, create an access token. Enable the "api" scope only.
2. Generate a random secret for your app, and save it for later. For example, you can use:
```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
```
3. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [Method 5](#run-as-a-github-app) steps 4-7.
4. In the secrets file, fill in the following:
- Your OpenAI key.
- In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token.
- Set deployment_type to 'gitlab' in [configuration.toml](./pr_agent/settings/configuration.toml)
5. Create a webhook in GitLab. Set the URL to the URL of your app's server. Set the secret token to the generated secret from step 2.
In the "Trigger" section, check the comments and merge request events boxes.
6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.
### Run as a Bitbucket Pipeline
You can use the Bitbucket Pipeline system to run PR-Agent on every pull request open or update.
1. Add the following file in your repository bitbucket_pipelines.yml
```yaml
pipelines:
pull-requests:
'**':
- step:
name: PR Agent Review
image: python:3.10
services:
- docker
script:
- docker run -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=https://bitbucket.org/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pull-requests/$BITBUCKET_PR_ID review
```
2. Add the following secure variables to your repository under Repository settings > Pipelines > Repository variables.
OPENAI_API_KEY: <your key>
BITBUCKET_BEARER_TOKEN: <your token>
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
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

@ -1,4 +1,4 @@
# Git Patch Logic # PR Compression Strategy
There are two scenarios: There are two scenarios:
1. The PR is small enough to fit in a single prompt (including system and user prompt) 1. The PR is small enough to fit in a single prompt (including system and user prompt)
2. The PR is too large to fit in a single prompt (including system and user prompt) 2. The PR is too large to fit in a single prompt (including system and user prompt)
@ -16,7 +16,7 @@ We prioritize the languages of the repo based on the following criteria:
## Small PR ## Small PR
In this case, we can fit the entire PR in a single prompt: In this case, we can fit the entire PR in a single prompt:
1. Exclude binary files and non code files (e.g. images, pdfs, etc) 1. Exclude binary files and non code files (e.g. images, pdfs, etc)
2. We Expand the surrounding context of each patch to 6 lines above and below the patch 2. We Expand the surrounding context of each patch to 3 lines above and below the patch
## Large PR ## Large PR
### Motivation ### Motivation
@ -25,7 +25,7 @@ We want to be able to pack as much information as possible in a single LMM promp
#### PR compression strategy #### Compression strategy
We prioritize additions over deletions: We prioritize additions over deletions:
- Combine all deleted files into a single list (`deleted files`) - Combine all deleted files into a single list (`deleted files`)
- File patches are a list of hunks, remove all hunks of type deletion-only from the hunks in the file patch - File patches are a list of hunks, remove all hunks of type deletion-only from the hunks in the file patch
@ -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>

202
README.md
View File

@ -2,32 +2,50 @@
<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>
[![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE) [![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613)
[![Twitter](https://img.shields.io/twitter/follow/codiumai)](https://twitter.com/codiumai)
<a href="https://github.com/Codium-ai/pr-agent/commits/main"> <a href="https://github.com/Codium-ai/pr-agent/commits/main">
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20"> <img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
</a> </a>
</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 PR feedback: 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:
**Auto-Description**: Automatically generating [PR description](https://github.com/Codium-ai/pr-agent/pull/229#issue-1860711415) - title, type, summary, code walkthrough and labels. **Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels.
\ \
**Auto Review**: [Adjustable feedback](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695022908) about the PR main theme, type, relevant tests, security issues, score, and various suggestions for the PR content. **Auto Review ([`/review`](./docs/REVIEW.md))**: Adjustable feedback about the PR main theme, type, relevant tests, security issues, score, and various suggestions for the PR content.
\ \
**Question Answering**: Answering [free-text questions](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021332) about the PR. **Question Answering ([`/ask ...`](./docs/ASK.md))**: Answering free-text questions about the PR.
\ \
**Code Suggestions**: [Committable code suggestions](https://github.com/Codium-ai/pr-agent/pull/229#discussion_r1306919276) for improving the PR. **Code Suggestions ([`/improve`](./docs/IMPROVE.md))**: Committable code suggestions for improving the PR.
\ \
**Update Changelog**: Automatically updating the CHANGELOG.md file with the [PR changes](https://github.com/Codium-ai/pr-agent/pull/168#discussion_r1282077645). **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
\
**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.
<h3>Example results:</h2> See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different 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 [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools.
<h3>Example results:</h3>
</div> </div>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1687561986">/describe:</a></h4> <h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1687561986">/describe:</a></h4>
<div align="center"> <div align="center">
@ -35,87 +53,104 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
<img src="https://www.codium.ai/images/describe-2.gif" width="800"> <img src="https://www.codium.ai/images/describe-2.gif" width="800">
</p> </p>
</div> </div>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901">/review:</a></h4> <h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901">/review:</a></h4>
<div align="center"> <div align="center">
<p float="center"> <p float="center">
<img src="https://www.codium.ai/images/review-2.gif" width="800"> <img src="https://www.codium.ai/images/review-2.gif" width="800">
</p> </p>
</div> </div>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>
<div align="center"> [//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>)
<p float="center">
<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800"> [//]: # (<div align="center">)
</p>
</div> [//]: # (<p float="center">)
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>
<div align="center"> [//]: # (<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800">)
<p float="center">
<img src="https://www.codium.ai/images/ask-2.gif" width="800"> [//]: # (</p>)
</p>
</div> [//]: # (</div>)
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695024952">/improve:</a></h4>
<div align="center"> [//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>)
<p float="center">
<img src="https://www.codium.ai/images/improve-2.gif" width="800"> [//]: # (<div align="center">)
</p>
</div> [//]: # (<p float="center">)
[//]: # (<img src="https://www.codium.ai/images/ask-2.gif" width="800">)
[//]: # (</p>)
[//]: # (</div>)
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695024952">/improve:</a></h4>)
[//]: # (<div align="center">)
[//]: # (<p float="center">)
[//]: # (<img src="https://www.codium.ai/images/improve-2.gif" width="800">)
[//]: # (</p>)
[//]: # (</div>)
<div align="left"> <div align="left">
## Table of Contents
- [Overview](#overview) - [Overview](#overview)
- [Try it now](#try-it-now) - [Try it now](#try-it-now)
- [Installation](#installation) - [Installation](#installation)
- [Configuration](./CONFIGURATION.md)
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Why use PR-Agent](#why-use-pr-agent) - [Why use PR-Agent?](#why-use-pr-agent)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Similar projects](#similar-projects)
</div> </div>
## Overview ## Overview
`PR-Agent` offers extensive pull request functionalities across various git providers: `PR-Agent` offers extensive pull request functionalities across various git providers:
| | | GitHub | Gitlab | Bitbucket | CodeCommit | | | | GitHub | Gitlab | Bitbucket | CodeCommit | Azure DevOps | Gerrit |
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:| |-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | | | | ⮑ Incremental | :white_check_mark: | | | | | |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Auto-Description | :white_check_mark: | :white_check_mark: | | | | | Auto-Description | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Improve Code | :white_check_mark: | :white_check_mark: | | | | | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | | | | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Reflect and Review | :white_check_mark: | | | | | | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: |
| | Update CHANGELOG.md | :white_check_mark: | | | | | | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| | | | | | | | | Find similar issue | :white_check_mark: | | | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | App / webhook | :white_check_mark: | :white_check_mark: | | | | | Generate Labels | :white_check_mark: | :white_check_mark: | | | | |
| | Tagging bot | :white_check_mark: | | | | | | | | | | | |
| | Actions | :white_check_mark: | | | | | USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | | | | | | | | App / webhook | :white_check_mark: | :white_check_mark: | | | |
| CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | Tagging bot | :white_check_mark: | | | | |
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | Actions | :white_check_mark: | | | | |
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | Web server | | | | | | :white_check_mark: |
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | | | | | | |
| | Incremental PR Review | :white_check_mark: | | | | | CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Incremental PR Review | :white_check_mark: | | | | | |
Examples for invoking the different tools via the CLI: Review the [usage guide](./Usage.md) section for detailed instructions how to use the different tools, select the relevant git provider (GitHub, Gitlab, Bitbucket,...), and adjust the configuration file to your needs.
- **Review**: python cli.py --pr_url=<pr_url> review
- **Describe**: python cli.py --pr_url=<pr_url> describe
- **Improve**: python cli.py --pr_url=<pr_url> improve
- **Ask**: python cli.py --pr_url=<pr_url> ask "Write me a poem about this PR"
- **Reflect**: python cli.py --pr_url=<pr_url> reflect
- **Update Changelog**: python cli.py --pr_url=<pr_url> update_changelog
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
In the [configuration](./CONFIGURATION.md) file you can select your git provider (GitHub, Gitlab, Bitbucket), and further configure the different tools.
## Try it now ## Try it now
Try GPT-4 powered PR-Agent on your public GitHub repository for free. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment! The agent will generate a response based on your command. 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.
For example, add a comment to any pull request with the following text:
```
@CodiumAI-Agent /review
```
and the agent will respond with a review of your PR
![Review generation process](https://www.codium.ai/images/demo-2.gif) ![Review generation process](https://www.codium.ai/images/demo-2.gif)
To set up your own PR-Agent, see the [Installation](#installation) section
To set up your own PR-Agent, see the [Installation](#installation) section below.
--- ---
@ -129,20 +164,22 @@ To get started with PR-Agent quickly, you first need to acquire two tokens:
There are several ways to use PR-Agent: There are several ways to use PR-Agent:
- [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required) - [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required)
- [Method 2: Run as a GitHub Action](INSTALL.md#method-2-run-as-a-github-action) - [Method 2: Run from source](INSTALL.md#method-2-run-from-source)
- [Method 3: Run from source](INSTALL.md#method-3-run-from-source) - [Method 3: Run as a GitHub Action](INSTALL.md#method-3-run-as-a-github-action)
- [Method 4: Run as a polling server](INSTALL.md#method-4-run-as-a-polling-server) - [Method 4: Run as a polling server](INSTALL.md#method-4-run-as-a-polling-server)
- Request reviews by tagging your GitHub user on a PR - Request reviews by tagging your GitHub user on a PR
- [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app) - [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app)
- Allowing you to automate the review process on your private or public repositories - Allowing you to automate the review process on your private or public repositories
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function) - [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup) - [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
- [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server)
- [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline)
## How it works ## How it works
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/07/codiumai-diagram-v4.jpg) ![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
@ -154,7 +191,7 @@ Here are some advantages of PR-Agent:
- We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably. - We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably.
- Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs. - Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs.
- Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](./CONFIGURATION.md) file. Adding additional categories is easy and accessible. - Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](pr_agent/settings/configuration.toml) file. Adding additional categories is easy and accessible.
- We support **multiple git providers** (GitHub, Gitlab, Bitbucket, CodeCommit), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2). - We support **multiple git providers** (GitHub, Gitlab, Bitbucket, CodeCommit), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2).
- We are open-source, and welcome contributions from the community. - We are open-source, and welcome contributions from the community.
@ -164,7 +201,7 @@ Here are some advantages of PR-Agent:
- [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172)) - [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172))
- [x] Develop additional logic for handling large PRs (see [here](https://github.com/Codium-ai/pr-agent/pull/229)) - [x] Develop additional logic for handling large PRs (see [here](https://github.com/Codium-ai/pr-agent/pull/229))
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags) - [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
- [ ] PR-Agent for issues, and just for pull requests - [x] PR-Agent for issues
- [ ] Adding more tools. Possible directions: - [ ] Adding more tools. Possible directions:
- [x] PR description - [x] PR description
- [x] Inline code suggestions - [x] Inline code suggestions
@ -172,13 +209,34 @@ Here are some advantages of PR-Agent:
- [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89)) - [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89))
- [ ] Enforcing CONTRIBUTING.md guidelines - [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues) - [ ] Performance (are there any performance issues)
- [ ] Documentation (is the PR properly documented) - [x] Documentation (is the PR properly documented)
- [ ] ... - [ ] ...
See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes.
## Similar Projects ## Similar Projects
- [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release) - [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release) (although various capabilities are much more advanced in the CodiumAI IDE plugins)
- [Aider - GPT powered coding in your terminal](https://github.com/paul-gauthier/aider) - [Aider - GPT powered coding in your terminal](https://github.com/paul-gauthier/aider)
- [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer) - [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer)
- [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview) - [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview)
- [AI-Maintainer](https://github.com/merwanehamadi/AI-Maintainer) - [AI-Maintainer](https://github.com/merwanehamadi/AI-Maintainer)
## Data Privacy
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
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
[![Join our Discord community](https://raw.githubusercontent.com/Codium-ai/codiumai-vscode-release/main/media/docs/Joincommunity.png)](https://discord.gg/kG35uSHDBc)
- Discord community: https://discord.gg/kG35uSHDBc
- CodiumAI site: https://codium.ai
- Blog: https://www.codium.ai/blog/
- Troubleshooting: https://www.codium.ai/blog/technical-faq-and-troubleshooting/
- Support: support@codium.ai

103
RELEASE_NOTES.md Normal file
View File

@ -0,0 +1,103 @@
## [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
- codiumai/pr-agent:0.10
- codiumai/pr-agent:0.10-github_app
- codiumai/pr-agent:0.10-bitbucket-app
- codiumai/pr-agent:0.10-gitlab_webhook
- codiumai/pr-agent:0.10-github_polling
- codiumai/pr-agent:0.10-github_action
### Added::Algo
- Review tool now works with [persistent comments](https://github.com/Codium-ai/pr-agent/pull/451) by default
- Bitbucket now publishes review suggestions with [code links](https://github.com/Codium-ai/pr-agent/pull/428)
- Enabling to limit [max number of tokens](https://github.com/Codium-ai/pr-agent/pull/437/files)
- Support ['gpt-4-1106-preview'](https://github.com/Codium-ai/pr-agent/pull/437/files) model
- Support for Google's [Vertex AI](https://github.com/Codium-ai/pr-agent/pull/436)
- Implementing [thresholds](https://github.com/Codium-ai/pr-agent/pull/423) for incremental PR reviews
- Decoupled custom labels from [PR type](https://github.com/Codium-ai/pr-agent/pull/431)
### Fixed
- Fixed bug in [parsing quotes](https://github.com/Codium-ai/pr-agent/pull/446) in CLI
- Preserve [user-added labels](https://github.com/Codium-ai/pr-agent/pull/433) in pull requests
- Bug fixes in GitLab and BitBucket
## [Version 0.9] - 2023-10-29
- codiumai/pr-agent:0.9
- codiumai/pr-agent:0.9-github_app
- codiumai/pr-agent:0.9-bitbucket-app
- codiumai/pr-agent:0.9-gitlab_webhook
- codiumai/pr-agent:0.9-github_polling
- codiumai/pr-agent:0.9-github_action
### Added::Algo
- New tool - [generate_labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/GENERATE_CUSTOM_LABELS.md)
- New ability to use [customize labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/GENERATE_CUSTOM_LABELS.md#how-to-enable-custom-labels) on the `review` and `describe` tools.
- New tool - [add_docs](https://github.com/Codium-ai/pr-agent/blob/main/docs/ADD_DOCUMENTATION.md)
- GitHub Action: Can now use a `.pr_agent.toml` file to control configuration parameters (see [Usage Guide](./Usage.md#working-with-github-action)).
- GitHub App: Added ability to trigger tools on [push events](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools-for-new-code-pr-push)
- Support custom domain URLs for Azure devops integration (see [link](https://github.com/Codium-ai/pr-agent/pull/381)).
- PR Description default mode is now in [bullet points](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L35).
### Added::Documentation
Significant documentation updates (see [Installation Guide](https://github.com/Codium-ai/pr-agent/blob/main/INSTALL.md), [Usage Guide](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md), and [Tools Guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md))
### Fixed
- Fixed support for BitBucket pipeline (see [link](https://github.com/Codium-ai/pr-agent/pull/386))
- Fixed a bug in `review -i` tool
- Added blacklist for specific file extensions in `add_docs` tool (see [link](https://github.com/Codium-ai/pr-agent/pull/385/))
## [Version 0.8] - 2023-09-27
- codiumai/pr-agent:0.8
- codiumai/pr-agent:0.8-github_app
- codiumai/pr-agent:0.8-bitbucket-app
- codiumai/pr-agent:0.8-gitlab_webhook
- codiumai/pr-agent:0.8-github_polling
- codiumai/pr-agent:0.8-github_action
### Added::Algo
- GitHub Action: Can control which tools will run automatically when a new PR is created. (see usage guide: https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
- Code suggestion tool: Will try to avoid an 'add comments' suggestion (see https://github.com/Codium-ai/pr-agent/pull/327)
### Fixed
- Gitlab: Fixed a bug of improper usage of pr_id
## [Version 0.7] - 2023-09-20
### Docker Tags
- codiumai/pr-agent:0.7
- codiumai/pr-agent:0.7-github_app
- codiumai/pr-agent:0.7-bitbucket-app
- codiumai/pr-agent:0.7-gitlab_webhook
- codiumai/pr-agent:0.7-github_polling
- codiumai/pr-agent:0.7-github_action
### Added::Algo
- New tool /similar_issue - Currently on GitHub app and CLI: indexes the issues in the repo, find the most similar issues to the target issue.
- Describe markers: Empower the /describe tool with a templating capability (see more details in https://github.com/Codium-ai/pr-agent/pull/273).
- New feature in the /review tool - added an estimated effort estimation to the review (https://github.com/Codium-ai/pr-agent/pull/306).
### Added::Infrastructure
- Implementation of a GitLab webhook.
- Implementation of a BitBucket app.
### Fixed
- Protection against no code suggestions generated.
- Resilience to repositories where the languages cannot be automatically detected.

419
Usage.md Normal file
View File

@ -0,0 +1,419 @@
## Usage Guide
### Table of Contents
- [Introduction](#introduction)
- [Working from a local repo (CLI)](#working-from-a-local-repo-cli)
- [Online usage](#online-usage)
- [Working with GitHub App](#working-with-github-app)
- [Working with GitHub Action](#working-with-github-action)
- [Working with BitBucket App](#working-with-bitbucket-self-hosted-app)
- [Changing a model](#changing-a-model)
- [Working with large PRs](#working-with-large-prs)
- [Appendix - additional configurations walkthrough](#appendix---additional-configurations-walkthrough)
### Introduction
After [installation](/INSTALL.md), there are three basic ways to invoke CodiumAI PR-Agent:
1. Locally running a CLI command
2. Online usage - by [commenting](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR
3. Enabling PR-Agent tools to run automatically when a new PR is opened
Specifically, CLI commands can be issued by invoking a pre-built [docker image](/INSTALL.md#running-from-source), or by invoking a [locally cloned repo](INSTALL.md#method-2-run-from-source).
For online usage, you will need to setup either a [GitHub App](INSTALL.md#method-5-run-as-a-github-app), or a [GitHub Action](INSTALL.md#method-3-run-as-a-github-action).
GitHub App and GitHub Action also enable to run PR-Agent specific tool automatically when a new PR is opened.
#### The configuration file
The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**.
In addition to general configuration options, each tool has its own configurations. For example, the `review` tool will use parameters from the [pr_reviewer](/pr_agent/settings/configuration.toml#L16) section in the configuration file.
The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the different tools and their configurations.
#### 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.
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.REGEX`
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
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:
`
"github", "gitlab", "azure", "codecommit", "local", "gerrit"
`
[//]: # (** online usage:**)
[//]: # (Options that are available in the configuration file can be specified at run time when calling actions. Two examples:)
[//]: # (```)
[//]: # (- /review --pr_reviewer.extra_instructions="focus on the file: ...")
[//]: # (- /describe --pr_description.add_original_user_description=false -pr_description.extra_instructions="make sure to mention: ...")
[//]: # (```)
### Working from a local repo (CLI)
When running from your local repo (CLI), your local configuration file will be used.
Examples of invoking the different tools via the CLI:
- **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review`
- **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe`
- **Improve**: `python -m pr_agent.cli --pr_url=<pr_url> improve`
- **Ask**: `python -m pr_agent.cli --pr_url=<pr_url> ask "Write me a poem about this PR"`
- **Reflect**: `python -m pr_agent.cli --pr_url=<pr_url> reflect`
- **Update Changelog**: `python -m pr_agent.cli --pr_url=<pr_url> update_changelog`
`<pr_url>` is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
**Notes:**
(1) in addition to editing your local configuration file, you can also change any configuration value by adding it to the command line:
```
python -m pr_agent.cli --pr_url=<pr_url> /review --pr_reviewer.extra_instructions="focus on the file: ..."
```
(2) You can print results locally, without publishing them, by setting in `configuration.toml`:
```
[config]
publish_output=true
verbosity_level=2
```
This is useful for debugging or experimenting with different tools.
### Online usage
Online usage means invoking PR-Agent tools by [comments](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR.
Commands for invoking the different tools via comments:
- **Review**: `/review`
- **Describe**: `/describe`
- **Improve**: `/improve`
- **Ask**: `/ask "..."`
- **Reflect**: `/reflect`
- **Update Changelog**: `/update_changelog`
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:
```
/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.
### 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.
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`:
```
[pr_reviewer]
num_code_suggestions=1
```
Then you will overwrite the default number of code suggestions to 1.
#### GitHub app automatic tools
The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations.
In this section you can define configurations to control the conditions for which tools will **run automatically**.
##### GitHub app automatic tools for PR actions
The GitHub app can respond to the following actions on a PR:
1. `opened` - Opening a new PR
2. `reopened` - Reopening a closed PR
3. `ready_for_review` - Moving a PR from Draft to Open
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 `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]
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/auto_review",
]
```
This means that when a new PR is opened/reopened or marked as ready for review, PR-Agent will run the `describe` and `auto_review` tools.
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
You can override the default tool parameters by uploading a local configuration file called `.pr_agent.toml` to the root of your repo.
For example, if your local `.pr_agent.toml` file contains:
```
[pr_description]
add_original_user_description = false
keep_original_user_title = false
```
When a new PR is opened, PR-Agent will run the `describe` tool with the above parameters.
To cancel the automatic run of all the tools, set:
```
[github_app]
handle_pr_actions = []
```
##### GitHub app automatic tools for new code (PR push)
In addition to running automatic tools when a PR is opened, the GitHub app can also respond to new code that is pushed to an open PR.
The configuration toggle `handle_push_trigger` can be used to enable this feature.
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the PR.
```
[github_app]
handle_push_trigger = true
push_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
]
```
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 `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 parameters by uploading a local configuration file to the root of your repo.
#### Editing the prompts
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder.
In practice, the prompts are loaded and stored as a standard setting object.
Hence, editing them is similar to editing any other configuration value - just place the relevant key in `.pr_agent.toml`file, and override the default value.
For example, if you want to edit the prompts of the [describe](./pr_agent/settings/pr_description_prompts.toml) tool, you can add the following to your `.pr_agent.toml` file:
```
[pr_description_prompt]
system="""
...
"""
user="""
...
"""
```
Note that the new prompt will need to generate an output compatible with the relevant [post-process function](./pr_agent/tools/pr_description.py#L137).
### Working with GitHub Action
You can configure settings in GitHub action by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file.
Specifically, start by setting the following environment variables:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} # Make sure to add your OpenAI key to your repo secrets
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Make sure to add your GitHub token to your repo secrets
github_action.auto_review: "true" # enable\disable auto review
github_action.auto_describe: "true" # enable\disable auto describe
github_action.auto_improve: "false" # enable\disable auto improve
```
`github_action.auto_review`, `github_action.auto_describe` and `github_action.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
If not set, the default option is that only the `review` tool will run automatically when a new PR is opened.
Note that you can give additional config parameters by adding environment variables to `.github/workflows/pr_agent.yml`, or by using a `.pr_agent.toml` file in the root of your repo, similar to the GitHub App usage.
For example, you can set an environment variable: `pr_description.add_original_user_description=false`, or add a `.pr_agent.toml` file with the following content:
```
[pr_description]
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.
### Changing a model
See [here](pr_agent/algo/__init__.py) for the list of available models.
To use a different model than the default (GPT-4), you need to edit [configuration file](pr_agent/settings/configuration.toml#L2).
For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions.
#### Azure
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_type = "azure"
api_version = '2023-05-15' # Check Azure documentation for the current API version
api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<your resource name>.openai.azure.com"
openai.deployment_id = "" # The deployment name you chose when you deployed the engine
```
and set in your configuration file:
```
[config]
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
```
#### Huggingface
**Local**
You can run Huggingface models locally through either [VLLM](https://docs.litellm.ai/docs/providers/vllm) or [Ollama](https://docs.litellm.ai/docs/providers/ollama)
E.g. to use a new Huggingface model locally via Ollama, set:
```
[__init__.py]
MAX_TOKENS = {
"model-name-on-ollama": <max_tokens>
}
e.g.
MAX_TOKENS={
...,
"ollama/llama2": 4096
}
[config] # in configuration.toml
model = "ollama/llama2"
[ollama] # in .secrets.toml
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**
To use a new model with Huggingface Inference Endpoints, for example, set:
```
[__init__.py]
MAX_TOKENS = {
"model-name-on-huggingface": <max_tokens>
}
e.g.
MAX_TOKENS={
...,
"meta-llama/Llama-2-7b-chat-hf": 4096
}
[config] # in configuration.toml
model = "huggingface/meta-llama/Llama-2-7b-chat-hf"
[huggingface] # in .secrets.toml
key = ... # your huggingface api key
api_base = ... # the base url for your huggingface inference endpoint
```
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
#### Replicate
To use Llama2 model with Replicate, for example, set:
```
[config] # in configuration.toml
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
[replicate] # in .secrets.toml
key = ...
```
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
#### Vertex AI
To use Google's Vertex AI platform and its associated models (chat-bison/codechat-bison) set:
```
[config] # in configuration.toml
model = "vertex_ai/codechat-bison"
fallback_models="vertex_ai/codechat-bison"
[vertexai] # in .secrets.toml
vertex_project = "my-google-cloud-project"
vertex_location = ""
```
Your [application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) will be used for authentication so there is no need to set explicit credentials in most environments.
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
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.
This mode provide a very good speed-quality-cost tradeoff, and can handle most PRs successfully.
When the PR is above the token limit, it employs a [PR Compression strategy](./PR_COMPRESSION.md).
However, for very large PRs, or in case you want to emphasize quality over speed and cost, there are 2 possible solutions:
1) [Use a model](#changing-a-model) with larger context, like GPT-32K, or claude-100K. This solution will be applicable for all the tools.
2) For the `/improve` tool, there is an ['extended' mode](./docs/IMPROVE.md) (`/improve --extended`),
which divides the PR to chunks, and process each chunk separately. With this mode, regardless of the model, no compression will be done (but for large PRs, multiple model calls may occur)
### Appendix - additional configurations walkthrough
#### Extra instructions
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
```
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
```
#### Patch Extra Lines
By default, around any change in your PR, git patch provides 3 lines of context above and below the change.
```
@@ -12,5 +12,5 @@ def func1():
code line that already existed in the file...
code line that already existed in the file...
code line that already existed in the file....
-code line that was removed in the PR
+new code line added in the PR
code line that already existed in the file...
code line that already existed in the file...
code line that already existed in the file...
```
For the `review`, `describe`, `ask` and `add_docs` tools, if the token budget allows, PR-Agent tries to increase the number of lines of context, via the parameter:
```
[config]
patch_extra_lines=3
```
Increasing this number provides more context to the model, but will also increase the token budget.
If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), PR-Agent automatically sets this number to 0, using the original git patch.
#### Azure DevOps provider
To use Azure DevOps provider use the following settings in configuration.toml:
```
[config]
git_provider="azure"
use_repo_settings_file=false
```
And use the following settings (you have to replace the values) in .secrets.toml:
```
[azure_devops]
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
pat = "YOUR_PAT_TOKEN"
```

View File

@ -14,10 +14,18 @@ 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"]
FROM base as gitlab_webhook
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/gitlab_webhook.py"]
FROM base as test FROM base as test
ADD requirements-dev.txt . ADD requirements-dev.txt .
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt RUN pip install -r requirements-dev.txt && rm requirements-dev.txt

15
docs/ADD_DOCUMENTATION.md Normal file
View File

@ -0,0 +1,15 @@
# Add Documentation Tool
The `add_docs` tool scans the PR code changes, and automatically suggests documentation for the undocumented code components (functions, classes, etc.).
It can be invoked manually by commenting on any PR:
```
/add_docs
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/add_docs_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/add_docs.png width="768"></kbd>
### 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`.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".

11
docs/ASK.md Normal file
View File

@ -0,0 +1,11 @@
# ASK Tool
The `ask` tool answers questions about the PR, based on the PR code changes.
It can be invoked manually by commenting on any PR:
```
/ask "..."
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/ask_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/ask.png width="768"></kbd>

70
docs/DESCRIBE.md Normal file
View File

@ -0,0 +1,70 @@
# Describe Tool
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:
```
/describe
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/describe_comment.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)
### Configuration options
Under the section 'pr_description', the [configuration file](./../pr_agent/settings/configuration.toml#L28) contains options to customize the 'describe' tool:
- `publish_labels`: if set to true, the tool will publish the labels to the PR. Default is true.
- `publish_description_as_comment`: if set to true, the tool will publish the description as a comment to the PR. If false, it will overwrite the origianl description. Default is false.
- `add_original_user_description`: if set to true, the tool will add the original user description to the generated description. 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 ...".
- 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.
- `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 enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
For example, if the PR original description was:
```
User content...
## PR Description:
pr_agent:summary
## PR Walkthrough:
pr_agent:walkthrough
```
The marker `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
##### Example:
```
env:
pr_description.use_description_markers: 'true'
```
<kbd><img src=https://codium.ai/images/pr_agent/describe_markers_before.png width="768"></kbd>
==>
<kbd><img src=https://codium.ai/images/pr_agent/describe_markers_after.png width="768"></kbd>
##### Configuration params:
- `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false.
- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true.

View File

@ -0,0 +1,41 @@
# Generate Custom Labels
The `generate_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes.
It can be invoked manually by commenting on any PR:
```
/generate_labels
```
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:
<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:
<kbd><img src=https://codium.ai/images/pr_agent/custom_label_published.png width="768"></kbd>
### How to enable custom labels
Note that in addition to the dedicated tool `generate_labels`, the custom labels will also be used by the `review` and `describe` tools.
#### CLI
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
#### GitHub Action and GitHub App
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the local `.pr_agent.toml` file in you repository.
#### Configuration changes
- Change `enable_custom_labels` to True: This will turn off the default labels and enable the custom labels provided in the custom_labels.toml file.
- Add the custom labels. It should be formatted as follows:
```
[config]
enable_custom_labels=true
[custom_labels."Custom Label Name"]
description = "Description of when AI should suggest this label"
[custom_labels."Custom Label 2"]
description = "Description of when AI should suggest this label 2"
```

57
docs/IMPROVE.md Normal file
View File

@ -0,0 +1,57 @@
# Improve Tool
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:
```
/improve
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/improve_comment.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)
An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR:
```
/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).
Hence, the total number of suggestions is proportional to the number of chunks, i.e., the size of the PR.
### Configuration options
Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/settings/configuration.toml#L40) contains options to customize the 'improve' tool:
- `num_code_suggestions`: number of code suggestions provided by the 'improve' tool. Default is 4.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- `rank_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is false.
#### params for '/improve --extended' mode
- `num_code_suggestions_per_chunk`: number of code suggestions provided by the 'improve' tool, per chunk. Default is 8.
- `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true.
- `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.
#### summarize mode
- `summarize`: if set to true, the tool will present the code suggestions in a compact way. Default is false.
In this mode, instead of presenting committable suggestions, the different suggestions will be combined into a single compact comment, with significantly smaller PR footprint.
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.
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
- Best quality will be obtained by using 'improve --extended' mode.

79
docs/REVIEW.md Normal file
View File

@ -0,0 +1,79 @@
# Review Tool
The `review` tool scans the PR code changes, and automatically generates a PR review.
It can be invoked manually by commenting on any PR:
```
/review
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/review_comment.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)
### Configuration options
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review' tool:
#### enable\\disable features
- `require_focused_review`: if set to true, the tool will add a section - 'is the PR a focused one'. Default is false.
- `require_score_review`: if set to true, the tool will add a section that scores the PR. Default is false.
- `require_tests_review`: if set to true, the tool will add a section that checks if the PR contains tests. Default is true.
- `require_security_review`: if set to true, the tool will add a section that checks if the PR contains security issues. Default is true.
- `require_estimate_effort_to_review`: if set to true, the tool will add a section that estimates thed effort needed to review the PR. Default is true.
#### general options
- `num_code_suggestions`: number of code suggestions provided by the 'review' tool. Default is 4.
- `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.
- `automatic_review`: if set to false, no automatic reviews will be done. Default is true.
- `remove_previous_review_comment`: if set to true, the tool will remove the previous review comment before adding a new one. Default is false.
- `persistent_comment`: if set to true, the review comment will be persistent, meaning that every new review request will edit the previous one. Default is true.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
#### review labels
- `enable_review_labels_security`: if set to true, the tool will publish a 'possible security issue' label if it detects a security issue. Default is true.
- `enable_review_labels_effort`: if set to true, the tool will publish a 'Review effort [1-5]: x' label. Default is false.
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
#### Incremental Mode
For an incremental review, which only considers changes since the last PR-Agent review, this can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again, the following command can be used:
```
/review -i
```
Note that the incremental mode is only available for GitHub.
<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.
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.
- `minimal_commits_for_incremental_review`: Minimal number of commits since the last review that are required to create incremental review.
If there are less than the specified number of commits since the last review, the tool will not perform any action.
Default is 0 - the tool will always run, no matter how many commits since the last review.
- `minimal_minutes_for_incremental_review`: Minimal number of minutes that need to pass since the last reviewed commit to create incremental review.
If less that the specified number of minutes have passed between the last reviewed commit and running this command, the tool will not perform any action.
Default is 0 - the tool will always run, no matter how much time have passed since the last reviewed commit.
- `require_all_thresholds_for_incremental_review`: If set to true, all the previous thresholds must be met for incremental review to run. If false, only one is enough to run the tool.
For example, if `minimal_commits_for_incremental_review=2` and `minimal_minutes_for_incremental_review=2`, and we have 3 commits since the last review, but the last reviewed commit is from 1 minute ago:
When `require_all_thresholds_for_incremental_review=true` the incremental review __will not__ run, because only 1 out of 2 conditions were met (we have enough commits but the last review is too recent),
but when `require_all_thresholds_for_incremental_review=false` the incremental review __will__ run, because one condition is enough (we have 3 commits which is more than the configured 2).
Default is false - the tool will run as long as at least once conditions is met.
#### PR Reflection
By invoking:
```
/reflect_and_review
```
The tool will first ask the author questions about the PR, and will guide the review based on his answers.
<kbd><img src=https://codium.ai/images/pr_agent/reflection_questions.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/reflection_answers.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
- 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.
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_reviewer_prompts.toml#L29). 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.
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
- Unlike the 'review' feature, which does a lot of things, the ['improve --extended'](./IMPROVE.md) feature is dedicated only to suggestions, and usually gives better results.

31
docs/SIMILAR_ISSUE.md Normal file
View File

@ -0,0 +1,31 @@
# Similar Issue Tool
The similar issue tool retrieves the most similar issues to the current issue.
It can be invoked manually by commenting on any PR:
```
/similar_issue
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue_original_issue.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue_comment.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).
To enable usage of the '**similar issue**' tool, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
```
[pinecone]
api_key = "..."
environment = "..."
```
These parameters can be obtained by registering to [Pinecone](https://app.pinecone.io/?sessionType=signup/).
### How to use:
- To invoke the 'similar issue' tool from **CLI**, run:
`python3 cli.py --issue_url=... similar_issue`
- To invoke the 'similar' issue tool via online usage, [comment](https://github.com/Codium-ai/pr-agent/issues/178#issuecomment-1716934893) on a PR:
`/similar_issue`
- You can also enable the 'similar issue' tool to run automatically when a new issue is opened, by adding it to the [pr_commands list in the github_app section](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L66)

11
docs/TOOLS_GUIDE.md Normal file
View File

@ -0,0 +1,11 @@
## Tools Guide
- [DESCRIBE](./DESCRIBE.md)
- [REVIEW](./REVIEW.md)
- [IMPROVE](./IMPROVE.md)
- [ASK](./ASK.md)
- [SIMILAR_ISSUE](./SIMILAR_ISSUE.md)
- [UPDATE CHANGELOG](./UPDATE_CHANGELOG.md)
- [ADD DOCUMENTATION](./ADD_DOCUMENTATION.md)
- [GENERATE CUSTOM LABELS](./GENERATE_CUSTOM_LABELS.md)
See the **[installation guide](/INSTALL.md)** for instructions on how to setup PR-Agent.

19
docs/UPDATE_CHANGELOG.md Normal file
View File

@ -0,0 +1,19 @@
# Update Changelog Tool
The `update_changelog` tool automatically updates the CHANGELOG.md file with the PR changes.
It can be invoked manually by commenting on any PR:
```
/update_changelog
```
For example:
<kbd><img src=https://codium.ai/images/pr_agent/update_changelog_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/update_changelog.png width="768"></kbd>
### Configuration options
Under the section 'pr_update_changelog', the [configuration file](./../pr_agent/settings/configuration.toml#L50) contains options to customize the 'update changelog' tool:
- `push_changelog_changes`: whether to push the changes to CHANGELOG.md, or just print them. Default is false (print only).
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,18 +1,18 @@
import logging
import os
import shlex import shlex
import tempfile
from pr_agent.algo.utils import update_settings_from_args from pr_agent.algo.utils import update_settings_from_args
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.tools.pr_add_docs import PRAddDocs
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_config import PRConfig
from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_generate_labels import PRGenerateLabels
from pr_agent.tools.pr_information_from_user import PRInformationFromUser from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
from pr_agent.tools.pr_similar_issue import PRSimilarIssue
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
from pr_agent.tools.pr_config import PRConfig
command2class = { command2class = {
"auto_review": PRReviewer, "auto_review": PRReviewer,
@ -30,6 +30,9 @@ command2class = {
"update_changelog": PRUpdateChangelog, "update_changelog": PRUpdateChangelog,
"config": PRConfig, "config": PRConfig,
"settings": PRConfig, "settings": PRConfig,
"similar_issue": PRSimilarIssue,
"add_docs": PRAddDocs,
"generate_labels": PRGenerateLabels,
} }
commands = list(command2class.keys()) commands = list(command2class.keys())
@ -40,33 +43,21 @@ class PRAgent:
async def handle_request(self, pr_url, request, notify=None) -> bool: async def handle_request(self, pr_url, request, notify=None) -> bool:
# First, apply repo specific settings if exists # First, apply repo specific settings if exists
if get_settings().config.use_repo_settings_file: apply_repo_settings(pr_url)
repo_settings_file = None
try:
git_provider = get_git_provider()(pr_url)
repo_settings = git_provider.get_repo_settings()
if repo_settings:
repo_settings_file = None
fd, repo_settings_file = tempfile.mkstemp(suffix='.toml')
os.write(fd, repo_settings)
get_settings().load_file(repo_settings_file)
finally:
if repo_settings_file:
try:
os.remove(repo_settings_file)
except Exception as e:
logging.error(f"Failed to remove temporary settings file {repo_settings_file}", e)
# Then, apply user specific settings if exists # Then, apply user specific settings if exists
if isinstance(request, str):
request = request.replace("'", "\\'") request = request.replace("'", "\\'")
lexer = shlex.shlex(request, posix=True) lexer = shlex.shlex(request, posix=True)
lexer.whitespace_split = True lexer.whitespace_split = True
action, *args = list(lexer) action, *args = list(lexer)
else:
action, *args = request
args = update_settings_from_args(args) args = update_settings_from_args(args)
action = action.lstrip("/").lower() action = action.lstrip("/").lower()
if action == "reflect_and_review" and not get_settings().pr_reviewer.ask_and_reflect: if action == "reflect_and_review":
action = "review" get_settings().pr_reviewer.ask_and_reflect = True
if action == "answer": if action == "answer":
if notify: if notify:
notify() notify()
@ -80,3 +71,4 @@ class PRAgent:
else: else:
return False return False
return True return True

View File

@ -1,4 +1,5 @@
MAX_TOKENS = { MAX_TOKENS = {
'text-embedding-ada-002': 8000,
'gpt-3.5-turbo': 4000, 'gpt-3.5-turbo': 4000,
'gpt-3.5-turbo-0613': 4000, 'gpt-3.5-turbo-0613': 4000,
'gpt-3.5-turbo-0301': 4000, 'gpt-3.5-turbo-0301': 4000,
@ -7,8 +8,17 @@ MAX_TOKENS = {
'gpt-4': 8000, 'gpt-4': 8000,
'gpt-4-0613': 8000, 'gpt-4-0613': 8000,
'gpt-4-32k': 32000, 'gpt-4-32k': 32000,
'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'claude-instant-1': 100000, 'claude-instant-1': 100000,
'claude-2': 100000, 'claude-2': 100000,
'command-nightly': 4096, 'command-nightly': 4096,
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096, 'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
'meta-llama/Llama-2-7b-chat-hf': 4096,
'vertex_ai/codechat-bison': 6144,
'vertex_ai/codechat-bison-32k': 32000,
'codechat-bison': 6144,
'codechat-bison-32k': 32000,
'anthropic.claude-v2': 100000,
'anthropic.claude-instant-v1': 100000,
'anthropic.claude-v1': 100000,
} }

View File

@ -1,12 +1,13 @@
import logging import os
import boto3
import litellm import litellm
import openai import openai
from litellm import acompletion from litellm import acompletion
from openai.error import APIError, RateLimitError, Timeout, TryAgain from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry from retry import retry
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
OPENAI_RETRIES = 5 OPENAI_RETRIES = 5
@ -23,11 +24,17 @@ class AiHandler:
Initializes the OpenAI API key and other settings from a configuration file. Initializes the OpenAI API key and other settings from a configuration file.
Raises a ValueError if the OpenAI key is missing. Raises a ValueError if the OpenAI key is missing.
""" """
try: self.azure = False
self.aws_bedrock_client = None
if get_settings().get("OPENAI.KEY", None):
openai.api_key = get_settings().openai.key openai.api_key = get_settings().openai.key
litellm.openai_key = get_settings().openai.key litellm.openai_key = get_settings().openai.key
litellm.debugger = get_settings().config.litellm_debugger if get_settings().get("litellm.use_client"):
self.azure = False litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
assert litellm_token, "LITELLM_TOKEN is required"
os.environ["LITELLM_TOKEN"] = litellm_token
litellm.use_client = True
if get_settings().get("OPENAI.ORG", None): if get_settings().get("OPENAI.ORG", None):
litellm.organization = get_settings().openai.org litellm.organization = get_settings().openai.org
if get_settings().get("OPENAI.API_TYPE", None): if get_settings().get("OPENAI.API_TYPE", None):
@ -48,8 +55,19 @@ class AiHandler:
litellm.replicate_key = get_settings().replicate.key litellm.replicate_key = get_settings().replicate.key
if get_settings().get("HUGGINGFACE.KEY", None): if get_settings().get("HUGGINGFACE.KEY", None):
litellm.huggingface_key = get_settings().huggingface.key litellm.huggingface_key = get_settings().huggingface.key
except AttributeError as e: if get_settings().get("HUGGINGFACE.API_BASE", None):
raise ValueError("OpenAI key is required") from e litellm.api_base = get_settings().huggingface.api_base
if get_settings().get("VERTEXAI.VERTEX_PROJECT", None):
litellm.vertex_project = get_settings().vertexai.vertex_project
litellm.vertex_location = get_settings().get(
"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):
@ -83,33 +101,37 @@ class AiHandler:
try: try:
deployment_id = self.deployment_id deployment_id = self.deployment_id
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.debug( get_logger().debug(
f"Generating completion with {model}" f"Generating completion with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}" f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
) )
response = await acompletion( if self.azure:
model=model, model = 'azure/' + model
deployment_id=deployment_id, messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
messages=[ kwargs = {
{"role": "system", "content": system}, "model": model,
{"role": "user", "content": user} "deployment_id": deployment_id,
], "messages": messages,
temperature=temperature, "temperature": temperature,
azure=self.azure, "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:
logging.error("Error during OpenAI inference: ", e) get_logger().error("Error during OpenAI inference: ", e)
raise raise
except (RateLimitError) as e: except (RateLimitError) as e:
logging.error("Rate limit error during OpenAI inference: ", e) get_logger().error("Rate limit error during OpenAI inference: ", e)
raise raise
except (Exception) as e: except (Exception) as e:
logging.error("Unknown error during OpenAI inference: ", e) get_logger().error("Unknown error during OpenAI inference: ", e)
raise TryAgain from e raise TryAgain from e
if response is None or len(response["choices"]) == 0: if response is None or len(response["choices"]) == 0:
raise TryAgain raise TryAgain
resp = response["choices"][0]['message']['content'] resp = response["choices"][0]['message']['content']
finish_reason = response["choices"][0]["finish_reason"] finish_reason = response["choices"][0]["finish_reason"]
print(resp, finish_reason) usage = response.get("usage")
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage)
return resp, finish_reason return resp, finish_reason

View File

@ -0,0 +1,36 @@
import fnmatch
import re
from pr_agent.config_loader import get_settings
def filter_ignored(files):
"""
Filter out files that match the ignore patterns.
"""
try:
# load regex patterns, and translate glob patterns to regex
patterns = get_settings().ignore.regex
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
compiled_patterns = []
for r in patterns:
try:
compiled_patterns.append(re.compile(r))
except re.error:
pass
# keep filenames that _don't_ match the ignore regex
for r in compiled_patterns:
files = [f for f in files if (f.filename and not r.match(f.filename))]
except Exception as e:
print(f"Could not filter file list: {e}")
return files

View File

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
import logging
import re import re
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import EDIT_TYPE
from pr_agent.log import get_logger
def extend_patch(original_file_str, patch_str, num_lines) -> str: def extend_patch(original_file_str, patch_str, num_lines) -> str:
@ -40,12 +42,16 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
extended_patch_lines.extend( extended_patch_lines.extend(
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines]) original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines])
res = list(match.groups())
for i in range(len(res)):
if res[i] is None:
res[i] = 0
try: try:
start1, size1, start2, size2 = map(int, match.groups()[:4]) start1, size1, start2, size2 = map(int, res[:4])
except: # '@@ -0,0 +1 @@' case except: # '@@ -0,0 +1 @@' case
start1, size1, size2 = map(int, match.groups()[:3]) start1, size1, size2 = map(int, res[:3])
start2 = 0 start2 = 0
section_header = match.groups()[4] section_header = res[4]
extended_start1 = max(1, start1 - num_lines) extended_start1 = max(1, start1 - num_lines)
extended_size1 = size1 + (start1 - extended_start1) + num_lines extended_size1 = size1 + (start1 - extended_start1) + num_lines
extended_start2 = max(1, start2 - num_lines) extended_start2 = max(1, start2 - num_lines)
@ -59,7 +65,7 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
extended_patch_lines.append(line) extended_patch_lines.append(line)
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.error(f"Failed to extend patch: {e}") get_logger().error(f"Failed to extend patch: {e}")
return patch_str return patch_str
# finish previous hunk # finish previous hunk
@ -110,7 +116,7 @@ def omit_deletion_hunks(patch_lines) -> str:
def handle_patch_deletions(patch: str, original_file_content_str: str, def handle_patch_deletions(patch: str, original_file_content_str: str,
new_file_content_str: str, file_name: str) -> str: new_file_content_str: str, file_name: str, edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN) -> str:
""" """
Handle entire file or deletion patches. Handle entire file or deletion patches.
@ -127,17 +133,17 @@ def handle_patch_deletions(patch: str, original_file_content_str: str,
str: The modified patch with deletion hunks omitted. str: The modified patch with deletion hunks omitted.
""" """
if not new_file_content_str: if not new_file_content_str and edit_type != EDIT_TYPE.ADDED:
# logic for handling deleted files - don't show patch, just show that the file was deleted # logic for handling deleted files - don't show patch, just show that the file was deleted
if get_settings().config.verbosity_level > 0: if get_settings().config.verbosity_level > 0:
logging.info(f"Processing file: {file_name}, minimizing deletion file") get_logger().info(f"Processing file: {file_name}, minimizing deletion file")
patch = None # file was deleted patch = None # file was deleted
else: else:
patch_lines = patch.splitlines() patch_lines = patch.splitlines()
patch_new = omit_deletion_hunks(patch_lines) patch_new = omit_deletion_hunks(patch_lines)
if patch != patch_new: if patch != patch_new:
if get_settings().config.verbosity_level > 0: if get_settings().config.verbosity_level > 0:
logging.info(f"Processing file: {file_name}, hunks were deleted") get_logger().info(f"Processing file: {file_name}, hunks were deleted")
patch = patch_new patch = patch_new
return patch return patch
@ -207,10 +213,15 @@ __old hunk__
old_content_lines = [] old_content_lines = []
if match: if match:
prev_header_line = header_line prev_header_line = header_line
res = list(match.groups())
for i in range(len(res)):
if res[i] is None:
res[i] = 0
try: try:
start1, size1, start2, size2 = map(int, match.groups()[:4]) start1, size1, start2, size2 = map(int, res[:4])
except: # '@@ -0,0 +1 @@' case except: # '@@ -0,0 +1 @@' case
start1, size1, size2 = map(int, match.groups()[:3]) start1, size1, size2 = map(int, res[:3])
start2 = 0 start2 = 0
elif line.startswith('+'): elif line.startswith('+'):

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()])
@ -42,6 +43,11 @@ def sort_files_by_main_languages(languages: Dict, files: list):
files_sorted = [] files_sorted = []
rest_files = {} rest_files = {}
# if no languages detected, put all files in the "Other" category
if not languages:
files_sorted = [({"language": "Other", "files": list(files_filtered)})]
return files_sorted
main_extensions_flat = [] main_extensions_flat = []
for ext in main_extensions: for ext in main_extensions:
main_extensions_flat.extend(ext) main_extensions_flat.extend(ext)

View File

@ -1,27 +1,29 @@
from __future__ import annotations from __future__ import annotations
import difflib import difflib
import logging
import re import re
import traceback import traceback
from typing import Any, Callable, List, Tuple from typing import Any, Callable, List, Tuple
from github import RateLimitExceededException from github import RateLimitExceededException
from pr_agent.algo import MAX_TOKENS
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.token_handler import TokenHandler, get_token_encoder from pr_agent.algo.file_filter import filter_ignored
from pr_agent.algo.token_handler import TokenHandler
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 from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
from pr_agent.log import get_logger
DELETED_FILES_ = "Deleted files:\n" DELETED_FILES_ = "Deleted files:\n"
MORE_MODIFIED_FILES_ = "More modified files:\n" MORE_MODIFIED_FILES_ = "Additional modified files (insufficient token budget to process):\n"
ADDED_FILES_ = "Additional added files (insufficient token budget to process):\n"
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000 OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600 OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600
PATCH_EXTRA_LINES = 3
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str, def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str,
add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str: add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str:
@ -44,31 +46,37 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
""" """
if disable_extra_lines: if disable_extra_lines:
global PATCH_EXTRA_LINES
PATCH_EXTRA_LINES = 0 PATCH_EXTRA_LINES = 0
else:
PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines
try: try:
diff_files = git_provider.get_diff_files() diff_files = git_provider.get_diff_files()
except RateLimitExceededException as e: except RateLimitExceededException as e:
logging.error(f"Rate limit exceeded for git provider API. original message {e}") get_logger().error(f"Rate limit exceeded for git provider API. original message {e}")
raise raise
diff_files = filter_ignored(diff_files)
# get pr languages # get pr languages
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files) pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
# generate a standard diff string, with patch extension # generate a standard diff string, with patch extension
patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(pr_languages, token_handler, patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(
add_line_numbers_to_hunks) pr_languages, token_handler, add_line_numbers_to_hunks, patch_extra_lines=PATCH_EXTRA_LINES)
# if we are under the limit, return the full diff # if we are under the limit, return the full diff
if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < MAX_TOKENS[model]: if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < get_max_tokens(model):
return "\n".join(patches_extended) return "\n".join(patches_extended)
# if we are over the limit, start pruning # if we are over the limit, start pruning
patches_compressed, modified_file_names, deleted_file_names = \ patches_compressed, modified_file_names, deleted_file_names, added_file_names = \
pr_generate_compressed_diff(pr_languages, token_handler, model, add_line_numbers_to_hunks) pr_generate_compressed_diff(pr_languages, token_handler, model, add_line_numbers_to_hunks)
final_diff = "\n".join(patches_compressed) final_diff = "\n".join(patches_compressed)
if added_file_names:
added_list_str = ADDED_FILES_ + "\n".join(added_file_names)
final_diff = final_diff + "\n\n" + added_list_str
if modified_file_names: if modified_file_names:
modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names) modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names)
final_diff = final_diff + "\n\n" + modified_list_str final_diff = final_diff + "\n\n" + modified_list_str
@ -80,7 +88,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
def pr_generate_extended_diff(pr_languages: list, def pr_generate_extended_diff(pr_languages: list,
token_handler: TokenHandler, token_handler: TokenHandler,
add_line_numbers_to_hunks: bool) -> Tuple[list, int, list]: add_line_numbers_to_hunks: bool,
patch_extra_lines: int = 0) -> Tuple[list, int, list]:
""" """
Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff
minimization techniques if needed. minimization techniques if needed.
@ -102,7 +111,7 @@ def pr_generate_extended_diff(pr_languages: list,
continue continue
# extend each patch with extra lines of context # extend each patch with extra lines of context
extended_patch = extend_patch(original_file_content_str, patch, num_lines=PATCH_EXTRA_LINES) extended_patch = extend_patch(original_file_content_str, patch, num_lines=patch_extra_lines)
full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n" full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n"
if add_line_numbers_to_hunks: if add_line_numbers_to_hunks:
@ -118,7 +127,7 @@ def pr_generate_extended_diff(pr_languages: list,
def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, model: str, def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, model: str,
convert_hunks_to_line_numbers: bool) -> Tuple[list, list, list]: convert_hunks_to_line_numbers: bool) -> Tuple[list, list, list, list]:
""" """
Generate a compressed diff string for a pull request, using diff minimization techniques to reduce the number of Generate a compressed diff string for a pull request, using diff minimization techniques to reduce the number of
tokens used. tokens used.
@ -144,6 +153,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
""" """
patches = [] patches = []
added_files_list = []
modified_files_list = [] modified_files_list = []
deleted_files_list = [] deleted_files_list = []
# sort each one of the languages in top_langs by the number of tokens in the diff # sort each one of the languages in top_langs by the number of tokens in the diff
@ -161,7 +171,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
# removing delete-only hunks # removing delete-only hunks
patch = handle_patch_deletions(patch, original_file_content_str, patch = handle_patch_deletions(patch, original_file_content_str,
new_file_content_str, file.filename) new_file_content_str, file.filename, file.edit_type)
if patch is None: if patch is None:
if not deleted_files_list: if not deleted_files_list:
total_tokens += token_handler.count_tokens(DELETED_FILES_) total_tokens += token_handler.count_tokens(DELETED_FILES_)
@ -175,17 +185,22 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
new_patch_tokens = token_handler.count_tokens(patch) new_patch_tokens = token_handler.count_tokens(patch)
# Hard Stop, no more tokens # Hard Stop, no more tokens
if total_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD: if total_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
logging.warning(f"File was fully skipped, no more tokens: {file.filename}.") get_logger().warning(f"File was fully skipped, no more tokens: {file.filename}.")
continue continue
# If the patch is too large, just show the file name # If the patch is too large, just show the file name
if total_tokens + new_patch_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD: if total_tokens + new_patch_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
# Current logic is to skip the patch if it's too large # Current logic is to skip the patch if it's too large
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens # TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
# until we meet the requirements # until we meet the requirements
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.warning(f"Patch too large, minimizing it, {file.filename}") get_logger().warning(f"Patch too large, minimizing it, {file.filename}")
if file.edit_type == EDIT_TYPE.ADDED:
if not added_files_list:
total_tokens += token_handler.count_tokens(ADDED_FILES_)
added_files_list.append(file.filename)
else:
if not modified_files_list: if not modified_files_list:
total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_) total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
modified_files_list.append(file.filename) modified_files_list.append(file.filename)
@ -200,9 +215,9 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
patches.append(patch_final) patches.append(patch_final)
total_tokens += token_handler.count_tokens(patch_final) total_tokens += token_handler.count_tokens(patch_final)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}") get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}")
return patches, modified_files_list, deleted_files_list return patches, modified_files_list, deleted_files_list, added_files_list
async def retry_with_fallback_models(f: Callable): async def retry_with_fallback_models(f: Callable):
@ -214,7 +229,7 @@ async def retry_with_fallback_models(f: Callable):
get_settings().set("openai.deployment_id", deployment_id) get_settings().set("openai.deployment_id", deployment_id)
return await f(model) return await f(model)
except Exception as e: except Exception as e:
logging.warning( get_logger().warning(
f"Failed to generate prediction with {model}" f"Failed to generate prediction with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}: " f"{(' from deployment ' + deployment_id) if deployment_id else ''}: "
f"{traceback.format_exc()}" f"{traceback.format_exc()}"
@ -267,7 +282,7 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
for file in diff_files: for file in diff_files:
if file.filename.strip() == relevant_file: if file.filename and (file.filename.strip() == relevant_file):
patch = file.patch patch = file.patch
patch_lines = patch.splitlines() patch_lines = patch.splitlines()
@ -311,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:
logging.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,
@ -363,9 +349,11 @@ def get_pr_multi_diffs(git_provider: GitProvider,
try: try:
diff_files = git_provider.get_diff_files() diff_files = git_provider.get_diff_files()
except RateLimitExceededException as e: except RateLimitExceededException as e:
logging.error(f"Rate limit exceeded for git provider API. original message {e}") get_logger().error(f"Rate limit exceeded for git provider API. original message {e}")
raise raise
diff_files = filter_ignored(diff_files)
# Sort files by main language # Sort files by main language
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files) pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
@ -381,7 +369,7 @@ def get_pr_multi_diffs(git_provider: GitProvider,
for file in sorted_files: for file in sorted_files:
if call_number > max_calls: if call_number > max_calls:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Reached max calls ({max_calls})") get_logger().info(f"Reached max calls ({max_calls})")
break break
original_file_content_str = file.base_file original_file_content_str = file.base_file
@ -391,26 +379,26 @@ def get_pr_multi_diffs(git_provider: GitProvider,
continue continue
# Remove delete-only hunks # Remove delete-only hunks
patch = handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file.filename) patch = handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file.filename, file.edit_type)
if patch is None: if patch is None:
continue continue
patch = convert_to_hunks_with_lines_numbers(patch, file) patch = convert_to_hunks_with_lines_numbers(patch, file)
new_patch_tokens = token_handler.count_tokens(patch) new_patch_tokens = token_handler.count_tokens(patch)
if patch and (total_tokens + new_patch_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD): if patch and (total_tokens + new_patch_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD):
final_diff = "\n".join(patches) final_diff = "\n".join(patches)
final_diff_list.append(final_diff) final_diff_list.append(final_diff)
patches = [] patches = []
total_tokens = token_handler.prompt_tokens total_tokens = token_handler.prompt_tokens
call_number += 1 call_number += 1
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Call number: {call_number}") get_logger().info(f"Call number: {call_number}")
if patch: if patch:
patches.append(patch) patches.append(patch)
total_tokens += new_patch_tokens total_tokens += new_patch_tokens
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}") get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}")
# Add the last chunk # Add the last chunk
if patches: if patches:

View File

@ -21,7 +21,7 @@ class TokenHandler:
method. method.
""" """
def __init__(self, pr, vars: dict, system, user): def __init__(self, pr=None, vars: dict = {}, system="", user=""):
""" """
Initializes the TokenHandler object. Initializes the TokenHandler object.
@ -32,6 +32,7 @@ class TokenHandler:
- user: The user string. - user: The user string.
""" """
self.encoder = get_token_encoder() self.encoder = get_token_encoder()
if pr is not None:
self.prompt_tokens = self._get_system_user_tokens(pr, self.encoder, vars, system, user) self.prompt_tokens = self._get_system_user_tokens(pr, self.encoder, vars, system, user)
def _get_system_user_tokens(self, pr, encoder, vars: dict, system, user): def _get_system_user_tokens(self, pr, encoder, vars: dict, system, user):

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import difflib import difflib
import json import json
import logging
import re import re
import textwrap import textwrap
from datetime import datetime from datetime import datetime
@ -10,7 +9,11 @@ from typing import Any, List
import yaml import yaml
from starlette_context import context from starlette_context import context
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
def get_setting(key: str) -> Any: def get_setting(key: str) -> Any:
@ -20,7 +23,7 @@ def get_setting(key: str) -> Any:
except Exception: except Exception:
return global_settings.get(key, None) return global_settings.get(key, None)
def convert_to_markdown(output_data: dict) -> str: def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
Args: Args:
@ -42,6 +45,7 @@ def convert_to_markdown(output_data: dict) -> str:
"General suggestions": "💡", "General suggestions": "💡",
"Insights from user's answers": "📝", "Insights from user's answers": "📝",
"Code feedback": "🤖", "Code feedback": "🤖",
"Estimated effort to review [1-5]": "⏱️",
} }
for key, value in output_data.items(): for key, value in output_data.items():
@ -49,27 +53,34 @@ def convert_to_markdown(output_data: dict) -> str:
continue continue
if isinstance(value, dict): if isinstance(value, dict):
markdown_text += f"## {key}\n\n" markdown_text += f"## {key}\n\n"
markdown_text += convert_to_markdown(value) markdown_text += convert_to_markdown(value, gfm_supported)
elif isinstance(value, list): elif isinstance(value, list):
emoji = emojis.get(key, "") emoji = emojis.get(key, "")
if key.lower() == 'code feedback': if key.lower() == 'code feedback':
markdown_text += f"\n\n- **<details><summary> { emoji } Code feedback:**</summary>\n\n" if gfm_supported:
markdown_text += f"\n\n- "
markdown_text += f"<details><summary> { emoji } Code feedback:</summary>\n\n"
else:
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 item in 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) markdown_text += parse_code_suggestion(item, 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':
if gfm_supported:
markdown_text += "</details>\n\n" markdown_text += "</details>\n\n"
else:
markdown_text += "\n\n"
elif value != 'n/a': elif value != 'n/a':
emoji = emojis.get(key, "") emoji = emojis.get(key, "")
markdown_text += f"- {emoji} **{key}:** {value}\n" markdown_text += f"- {emoji} **{key}:** {value}\n"
return markdown_text return markdown_text
def parse_code_suggestion(code_suggestions: dict) -> str: def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
@ -89,9 +100,13 @@ def parse_code_suggestion(code_suggestions: dict) -> str:
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n" markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
else: else:
if "relevant file" in sub_key.lower(): if "relevant file" in sub_key.lower():
markdown_text += f"\n - **{sub_key}:** {sub_value}\n" markdown_text += f"\n - **{sub_key}:** {sub_value} \n"
else: else:
markdown_text += f" **{sub_key}:** {sub_value}\n" 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 and bitbucker
markdown_text += "\n" markdown_text += "\n"
return markdown_text return markdown_text
@ -149,7 +164,7 @@ def try_fix_json(review, max_iter=10, code_suggestions=False):
iter_count += 1 iter_count += 1
if not valid_json: if not valid_json:
logging.error("Unable to decode JSON response from AI") get_logger().error("Unable to decode JSON response from AI")
data = {} data = {}
return data return data
@ -220,7 +235,7 @@ def load_large_diff(filename, new_file_content_str: str, original_file_content_s
diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True), diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
new_file_content_str.splitlines(keepends=True)) new_file_content_str.splitlines(keepends=True))
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.") get_logger().warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.")
patch = ''.join(diff) patch = ''.join(diff)
except Exception: except Exception:
pass pass
@ -252,12 +267,12 @@ def update_settings_from_args(args: List[str]) -> List[str]:
vals = arg.split('=', 1) vals = arg.split('=', 1)
if len(vals) != 2: if len(vals) != 2:
if len(vals) > 2: # --extended is a valid argument if len(vals) > 2: # --extended is a valid argument
logging.error(f'Invalid argument format: {arg}') get_logger().error(f'Invalid argument format: {arg}')
other_args.append(arg) other_args.append(arg)
continue continue
key, value = _fix_key_value(*vals) key, value = _fix_key_value(*vals)
get_settings().set(key, value) get_settings().set(key, value)
logging.info(f'Updated setting {key} to: "{value}"') get_logger().info(f'Updated setting {key} to: "{value}"')
else: else:
other_args.append(arg) other_args.append(arg)
return other_args return other_args
@ -269,28 +284,142 @@ 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:
logging.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:
logging.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')
data = {}
for i in range(1, len(review_text_lines)): keys = ['relevant line:', 'suggestion content:', 'relevant file:']
review_text_lines_tmp = '\n'.join(review_text_lines[:-i]) # first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
response_text_lines_copy = response_text_lines.copy()
for i in range(0, len(response_text_lines_copy)):
for key in keys:
if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]:
response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}',
f'{key} |-\n ')
try: try:
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader) data = yaml.safe_load('\n'.join(response_text_lines_copy))
logging.info(f"Successfully parsed AI prediction after removing {i} lines") get_logger().info(f"Successfully parsed AI prediction after adding |-\n")
return data
except:
get_logger().info(f"Failed to parse AI prediction after adding |-\n")
# second fallback - try to remove last lines
data = {}
for i in range(1, len(response_text_lines)):
response_text_lines_tmp = '\n'.join(response_text_lines[:-i])
try:
data = yaml.safe_load(response_text_lines_tmp,)
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
break break
except: except:
pass pass
# 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 return data
except:
pass
def set_custom_labels(variables):
if not get_settings().config.enable_custom_labels:
return
labels = get_settings().custom_labels
if not labels:
# set default labels
labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
labels_list = "\n - ".join(labels) if labels else ""
labels_list = f" - {labels_list}" if labels_list else ""
variables["custom_labels"] = labels_list
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]}"
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):
"""
Only keep labels that has been added by the user
"""
try:
if current_labels is None:
current_labels = []
user_labels = []
for label in current_labels:
if label.lower() in ['bug fix', 'tests', 'enhancement', 'documentation', 'other']:
continue
if get_settings().config.enable_custom_labels:
if label in get_settings().custom_labels:
continue
user_labels.append(label)
if user_labels:
get_logger().info(f"Keeping user labels: {user_labels}")
except Exception as e:
get_logger().exception(f"Failed to get user labels: {e}")
return current_labels
return user_labels
def get_max_tokens(model):
settings = get_settings()
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:
max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model)
# get_logger().debug(f"limiting max tokens to {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

@ -1,10 +1,13 @@
import argparse import argparse
import asyncio import asyncio
import logging
import os import os
from pr_agent.agent.pr_agent import PRAgent, commands from pr_agent.agent.pr_agent import PRAgent, commands
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.log import setup_logger
setup_logger()
def run(inargs=None): def run(inargs=None):
@ -17,34 +20,46 @@ For example:
- cli.py --pr_url=... improve - cli.py --pr_url=... improve
- cli.py --pr_url=... ask "write me a poem about this PR" - cli.py --pr_url=... ask "write me a poem about this PR"
- cli.py --pr_url=... reflect - cli.py --pr_url=... reflect
- 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:
To edit any configuration parameter from 'configuration.toml', just add -config_path=<value>. To edit any configuration parameter from 'configuration.toml', just add -config_path=<value>.
For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions="focus on the file: ..."' For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions="focus on the file: ..."'
""") """)
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True) parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', default=None)
parser.add_argument('--issue_url', type=str, help='The URL of the Issue to review', default=None)
parser.add_argument('command', type=str, help='The', choices=commands, default='review') parser.add_argument('command', type=str, help='The', choices=commands, default='review')
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
args = parser.parse_args(inargs) args = parser.parse_args(inargs)
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) if not args.pr_url and not args.issue_url:
parser.print_help()
return
command = args.command.lower() command = args.command.lower()
get_settings().set("CONFIG.CLI_MODE", True) get_settings().set("CONFIG.CLI_MODE", True)
result = asyncio.run(PRAgent().handle_request(args.pr_url, command + " " + " ".join(args.rest))) if args.issue_url:
result = asyncio.run(PRAgent().handle_request(args.issue_url, [command] + args.rest))
else:
result = asyncio.run(PRAgent().handle_request(args.pr_url, [command] + args.rest))
if not result: if not result:
parser.print_help() parser.print_help()

View File

@ -14,6 +14,7 @@ global_settings = Dynaconf(
settings_files=[join(current_dir, f) for f in [ settings_files=[join(current_dir, f) for f in [
"settings/.secrets.toml", "settings/.secrets.toml",
"settings/configuration.toml", "settings/configuration.toml",
"settings/ignore.toml",
"settings/language_extensions.toml", "settings/language_extensions.toml",
"settings/pr_reviewer_prompts.toml", "settings/pr_reviewer_prompts.toml",
"settings/pr_questions_prompts.toml", "settings/pr_questions_prompts.toml",
@ -22,7 +23,10 @@ global_settings = Dynaconf(
"settings/pr_sort_code_suggestions_prompts.toml", "settings/pr_sort_code_suggestions_prompts.toml",
"settings/pr_information_from_user_prompts.toml", "settings/pr_information_from_user_prompts.toml",
"settings/pr_update_changelog_prompts.toml", "settings/pr_update_changelog_prompts.toml",
"settings_prod/.secrets.toml" "settings/pr_custom_labels.toml",
"settings/pr_add_docs.toml",
"settings_prod/.secrets.toml",
"settings/custom_labels.toml"
]] ]]
) )

View File

@ -1,18 +1,23 @@
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
from pr_agent.git_providers.local_git_provider import LocalGitProvider from pr_agent.git_providers.local_git_provider import LocalGitProvider
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
from pr_agent.git_providers.gerrit_provider import GerritProvider
_GIT_PROVIDERS = { _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,
'gerrit': GerritProvider,
} }
def get_git_provider(): def get_git_provider():

View File

@ -1,10 +1,11 @@
import json import json
import logging
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
import os import os
from ..log import get_logger
AZURE_DEVOPS_AVAILABLE = True AZURE_DEVOPS_AVAILABLE = True
try: try:
from msrest.authentication import BasicAuthentication from msrest.authentication import BasicAuthentication
@ -13,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
@ -38,7 +38,8 @@ class AzureDevopsProvider:
self.set_pr(pr_url) self.set_pr(pr_url)
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels', 'remove_initial_comment']: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels',
'remove_initial_comment', 'gfm_markdown']:
return False return False
return True return True
@ -54,7 +55,7 @@ class AzureDevopsProvider:
path=".pr_agent.toml") path=".pr_agent.toml")
return contents return contents
except Exception as e: except Exception as e:
logging.exception("get repo settings error") get_logger().exception("get repo settings error")
return "" return ""
def get_files(self): def get_files(self):
@ -87,6 +88,8 @@ class AzureDevopsProvider:
changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug, changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug,
repository_id=self.repo_slug, commit_id=c.commit_id) repository_id=self.repo_slug, commit_id=c.commit_id)
for i in changes_obj.changes: for i in changes_obj.changes:
if(i['item']['gitObjectType'] == 'tree'):
continue
diffs.append(i['item']['path']) diffs.append(i['item']['path'])
diff_types[i['item']['path']] = i['changeType'] diff_types[i['item']['path']] = i['changeType']
@ -97,6 +100,7 @@ class AzureDevopsProvider:
continue continue
version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit') version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit')
try:
new_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug, new_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
path=file, path=file,
project=self.workspace_slug, project=self.workspace_slug,
@ -105,6 +109,9 @@ class AzureDevopsProvider:
include_content=True) include_content=True)
new_file_content_str = new_file_content_str.content new_file_content_str = new_file_content_str.content
except Exception as error:
get_logger().error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(error))
new_file_content_str = ""
edit_type = EDIT_TYPE.MODIFIED edit_type = EDIT_TYPE.MODIFIED
if diff_types[file] == 'add': if diff_types[file] == 'add':
@ -115,6 +122,7 @@ class AzureDevopsProvider:
edit_type = EDIT_TYPE.RENAMED edit_type = EDIT_TYPE.RENAMED
version = GitVersionDescriptor(version=base_sha.commit_id, version_type='commit') version = GitVersionDescriptor(version=base_sha.commit_id, version_type='commit')
try:
original_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug, original_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
path=file, path=file,
project=self.workspace_slug, project=self.workspace_slug,
@ -122,6 +130,9 @@ class AzureDevopsProvider:
download=False, download=False,
include_content=True) include_content=True)
original_file_content_str = original_file_content_str.content original_file_content_str = original_file_content_str.content
except Exception as error:
get_logger().error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(error))
original_file_content_str = ""
patch = load_large_diff(file, new_file_content_str, original_file_content_str) patch = load_large_diff(file, new_file_content_str, original_file_content_str)
@ -155,7 +166,7 @@ class AzureDevopsProvider:
pull_request_id=self.pr_num, pull_request_id=self.pr_num,
git_pull_request_to_update=updated_pr) git_pull_request_to_update=updated_pr)
except Exception as e: except Exception as e:
logging.exception(f"Could not update pull request {self.pr_num} description: {e}") get_logger().exception(f"Could not update pull request {self.pr_num} description: {e}")
def remove_initial_comment(self): def remove_initial_comment(self):
return "" # not implemented yet return "" # not implemented yet
@ -224,9 +235,6 @@ class AzureDevopsProvider:
def _parse_pr_url(pr_url: str) -> Tuple[str, int]: def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_url)
if 'azure.com' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid Azure DevOps URL")
path_parts = parsed_url.path.strip('/').split('/') path_parts = parsed_url.path.strip('/').split('/')
if len(path_parts) < 6 or path_parts[4] != 'pullrequest': if len(path_parts) < 6 or path_parts[4] != 'pullrequest':

View File

@ -1,5 +1,4 @@
import json import json
import logging
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
@ -7,8 +6,10 @@ import requests
from atlassian.bitbucket import Cloud from atlassian.bitbucket import Cloud
from starlette_context import context from starlette_context import context
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..config_loader import get_settings from ..config_loader import get_settings
from .git_provider import FilePatchInfo, GitProvider from ..log import get_logger
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
class BitbucketProvider(GitProvider): class BitbucketProvider(GitProvider):
@ -31,19 +32,23 @@ class BitbucketProvider(GitProvider):
self.repo = None self.repo = None
self.pr_num = None self.pr_num = None
self.pr = None self.pr = None
self.pr_url = pr_url
self.temp_comments = [] self.temp_comments = []
self.incremental = incremental self.incremental = incremental
self.diff_files = None
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"][ self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"]["comments"]["href"]
"comments" self.bitbucket_pull_request_api_url = self.pr._BitbucketBase__data["links"]['self']['href']
]["href"]
def get_repo_settings(self): def get_repo_settings(self):
try: try:
contents = self.repo_obj.get_contents( url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/"
".pr_agent.toml", ref=self.pr.head.sha f"{self.pr.destination_branch}/.pr_agent.toml")
).decoded_content response = requests.request("GET", url, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text.encode('utf-8')
return contents return contents
except Exception: except Exception:
return "" return ""
@ -61,14 +66,14 @@ class BitbucketProvider(GitProvider):
if not relevant_lines_start or relevant_lines_start == -1: if not relevant_lines_start or relevant_lines_start == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.exception( get_logger().exception(
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}" f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
) )
continue continue
if relevant_lines_end < relevant_lines_start: if relevant_lines_end < relevant_lines_start:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.exception( get_logger().exception(
f"Failed to publish code suggestion, " f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and " f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}" f"relevant_lines_start is {relevant_lines_start}"
@ -97,16 +102,11 @@ class BitbucketProvider(GitProvider):
return True return True
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.error(f"Failed to publish code suggestion, error: {e}") get_logger().error(f"Failed to publish code suggestion, error: {e}")
return False return False
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in [ if capability in ['get_issue_comments', 'publish_inline_comments', 'get_labels', 'gfm_markdown']:
"get_issue_comments",
"create_inline_comment",
"publish_inline_comments",
"get_labels",
]:
return False return False
return True return True
@ -118,6 +118,9 @@ class BitbucketProvider(GitProvider):
return [diff.new.path for diff in self.pr.diffstat()] return [diff.new.path for diff in self.pr.diffstat()]
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files:
return self.diff_files
diffs = self.pr.diffstat() diffs = self.pr.diffstat()
diff_split = [ diff_split = [
"diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip() "diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip()
@ -129,16 +132,56 @@ class BitbucketProvider(GitProvider):
diff.old.get_data("links") diff.old.get_data("links")
) )
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links")) new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
diff_files.append( file_patch_canonic_structure = FilePatchInfo(
FilePatchInfo(
original_file_content_str, original_file_content_str,
new_file_content_str, new_file_content_str,
diff_split[index], diff_split[index],
diff.new.path, diff.new.path,
) )
)
if diff.data['status'] == 'added':
file_patch_canonic_structure.edit_type = EDIT_TYPE.ADDED
elif diff.data['status'] == 'removed':
file_patch_canonic_structure.edit_type = EDIT_TYPE.DELETED
elif diff.data['status'] == 'modified':
file_patch_canonic_structure.edit_type = EDIT_TYPE.MODIFIED
elif diff.data['status'] == 'renamed':
file_patch_canonic_structure.edit_type = EDIT_TYPE.RENAMED
diff_files.append(file_patch_canonic_structure)
self.diff_files = diff_files
return diff_files return diff_files
def get_latest_commit_url(self):
return self.pr.data['source']['commit']['links']['html']['href']
def get_comment_url(self, comment):
return comment.data['links']['html']['href']
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
try:
for comment in self.pr.comments():
body = comment.raw
if initial_header in body:
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
d = {"content": {"raw": pr_comment_updated}}
response = comment._update_data(comment.put(None, data=d))
self.publish_comment(
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
comment = self.pr.comment(pr_comment) comment = self.pr.comment(pr_comment)
if is_temporary: if is_temporary:
@ -147,31 +190,82 @@ class BitbucketProvider(GitProvider):
def remove_initial_comment(self): def remove_initial_comment(self):
try: try:
for comment in self.temp_comments: for comment in self.temp_comments:
self.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove temp comments, error: {e}")
def remove_comment(self, comment):
try:
self.pr.delete(f"comments/{comment}") self.pr.delete(f"comments/{comment}")
except Exception as e: except Exception as e:
logging.exception(f"Failed to remove temp comments, error: {e}") get_logger().exception(f"Failed to remove comment, error: {e}")
def publish_inline_comment( # funtion to create_inline_comment
self, comment: str, from_line: int, to_line: int, file: str 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)
payload = json.dumps( 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 = json.dumps( {
"content": { "content": {
"raw": comment, "raw": comment,
}, },
"inline": {"to": from_line, "path": file}, "inline": {
} "to": from_line,
) "path": file
},
})
response = requests.request( response = requests.request(
"POST", self.bitbucket_comment_api_url, data=payload, headers=self.headers "POST", self.bitbucket_comment_api_url, data=payload, headers=self.headers
) )
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:
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]): def publish_inline_comments(self, comments: list[dict]):
for comment in comments: for comment in comments:
self.publish_inline_comment( if 'position' in comment:
comment["body"], comment["start_line"], comment["line"], comment["path"] 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
@ -239,15 +333,26 @@ class BitbucketProvider(GitProvider):
def get_commit_messages(self): def get_commit_messages(self):
return "" # not implemented yet return "" # not implemented yet
def publish_description(self, pr_title: str, pr_body: str): # bitbucket does not support labels
def publish_description(self, pr_title: str, description: str):
payload = json.dumps({
"description": description,
"title": pr_title
})
response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
try:
if response.status_code != 200:
get_logger().info(f"Failed to update description, error code: {response.status_code}")
except:
pass pass
def create_inline_comment( return response
self, body: str, relevant_file: str, relevant_line_in_file: str
): # bitbucket does not support labels
pass def publish_labels(self, pr_types: list):
def publish_labels(self, labels):
pass pass
# bitbucket does not support labels
def get_labels(self): def get_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_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

@ -54,17 +54,22 @@ class CodeCommitClient:
def __init__(self): def __init__(self):
self.boto_client = None self.boto_client = None
def is_supported(self, capability: str) -> bool:
if capability in ["gfm_markdown"]:
return False
return True
def _connect_boto_client(self): def _connect_boto_client(self):
try: try:
self.boto_client = boto3.client("codecommit") self.boto_client = boto3.client("codecommit")
except Exception as e: except Exception as e:
raise ValueError(f"Failed to connect to AWS CodeCommit: {e}") raise ValueError(f"Failed to connect to AWS CodeCommit: {e}") from e
def get_differences(self, repo_name: int, destination_commit: str, source_commit: str): def get_differences(self, repo_name: int, destination_commit: str, source_commit: str):
""" """
Get the differences between two commits in CodeCommit. Get the differences between two commits in CodeCommit.
Parameters: Args:
- repo_name: Name of the repository - repo_name: Name of the repository
- destination_commit: Commit hash you want to merge into (the "before" hash) (usually on the main or master branch) - destination_commit: Commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
- source_commit: Commit hash of the code you are adding (the "after" branch) - source_commit: Commit hash of the code you are adding (the "after" branch)
@ -73,8 +78,8 @@ class CodeCommitClient:
- List of CodeCommitDifferencesResponse objects - List of CodeCommitDifferencesResponse objects
Boto3 Documentation: Boto3 Documentation:
aws codecommit get-differences - aws codecommit get-differences
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()
@ -90,7 +95,11 @@ class CodeCommitClient:
): ):
differences.extend(page.get("differences", [])) differences.extend(page.get("differences", []))
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
raise ValueError(f"Failed to retrieve differences from CodeCommit PR #{self.pr_num}") from e if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
raise ValueError(f"CodeCommit cannot retrieve differences: Repository does not exist: {repo_name}") from e
raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e
except Exception as e:
raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e
output = [] output = []
for json in differences: for json in differences:
@ -101,7 +110,7 @@ class CodeCommitClient:
""" """
Retrieve a file from CodeCommit. Retrieve a file from CodeCommit.
Parameters: Args:
- repo_name: Name of the repository - repo_name: Name of the repository
- file_path: Path to the file you are retrieving - file_path: Path to the file you are retrieving
- sha_hash: Commit hash of the file you are retrieving - sha_hash: Commit hash of the file you are retrieving
@ -110,8 +119,8 @@ class CodeCommitClient:
- File contents - File contents
Boto3 Documentation: Boto3 Documentation:
aws codecommit get_file - aws codecommit get_file
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
""" """
if not file_path: if not file_path:
return "" return ""
@ -122,6 +131,8 @@ class CodeCommitClient:
try: try:
response = self.boto_client.get_file(repositoryName=repo_name, commitSpecifier=sha_hash, filePath=file_path) response = self.boto_client.get_file(repositoryName=repo_name, commitSpecifier=sha_hash, filePath=file_path)
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e
# if the file does not exist, but is flagged as optional, then return an empty string # if the file does not exist, but is flagged as optional, then return an empty string
if optional and e.response["Error"]["Code"] == 'FileDoesNotExistException': if optional and e.response["Error"]["Code"] == 'FileDoesNotExistException':
return "" return ""
@ -133,19 +144,20 @@ class CodeCommitClient:
return response.get("fileContent", "") return response.get("fileContent", "")
def get_pr(self, pr_number: int): def get_pr(self, repo_name: str, pr_number: int):
""" """
Get a information about a CodeCommit PR. Get a information about a CodeCommit PR.
Parameters: Args:
- repo_name: Name of the repository
- pr_number: The PR number you are requesting - pr_number: The PR number you are requesting
Returns: Returns:
- CodeCommitPullRequestResponse object - CodeCommitPullRequestResponse object
Boto3 Documentation: Boto3 Documentation:
aws codecommit get_pull_request - aws codecommit get_pull_request
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()
@ -155,6 +167,8 @@ class CodeCommitClient:
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException': if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
raise ValueError(f"CodeCommit cannot retrieve PR: PR number does not exist: {pr_number}") from e raise ValueError(f"CodeCommit cannot retrieve PR: PR number does not exist: {pr_number}") from e
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}: boto client error") from e raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}: boto client error") from e
except Exception as e: except Exception as e:
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}") from e raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}") from e
@ -164,28 +178,88 @@ class CodeCommitClient:
return CodeCommitPullRequestResponse(response.get("pullRequest", {})) return CodeCommitPullRequestResponse(response.get("pullRequest", {}))
def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str): def publish_description(self, pr_number: int, pr_title: str, pr_body: str):
""" """
Publish a comment to a pull request Set the title and description on a pull request
Parameters: Args:
- repo_name: name of the repository - pr_number: the AWS CodeCommit pull request number
- pr_number: number of the pull request - pr_title: title of the pull request
- destination_commit: The commit hash you want to merge into (the "before" hash) (usually on the main or master branch) - pr_body: body of the pull request
- source_commit: The commit hash of the code you are adding (the "after" branch)
- pr_comment: comment
Returns: Returns:
- None - None
Boto3 Documentation: Boto3 Documentation:
aws codecommit post_comment_for_pull_request - aws codecommit update_pull_request_title
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_title.html
- aws codecommit update_pull_request_description
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_description.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()
try: try:
self.boto_client.update_pull_request_title(pullRequestId=str(pr_number), title=pr_title)
self.boto_client.update_pull_request_description(pullRequestId=str(pr_number), description=pr_body)
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
raise ValueError(f"PR number does not exist: {pr_number}") from e
if e.response["Error"]["Code"] == 'InvalidTitleException':
raise ValueError(f"Invalid title for PR number: {pr_number}") from e
if e.response["Error"]["Code"] == 'InvalidDescriptionException':
raise ValueError(f"Invalid description for PR number: {pr_number}") from e
if e.response["Error"]["Code"] == 'PullRequestAlreadyClosedException':
raise ValueError(f"PR is already closed: PR number: {pr_number}") from e
raise ValueError(f"Boto3 client error calling publish_description") from e
except Exception as e:
raise ValueError(f"Error calling publish_description") from e
def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str, annotation_file: str = None, annotation_line: int = None):
"""
Publish a comment to a pull request
Args:
- repo_name: name of the repository
- pr_number: number of the pull request
- destination_commit: The commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
- source_commit: The commit hash of the code you are adding (the "after" branch)
- comment: The comment you want to publish
- annotation_file: The file you want to annotate (optional)
- annotation_line: The line number you want to annotate (optional)
Comment annotations for CodeCommit are different than GitHub.
CodeCommit only designates the starting line number for the comment.
It does not support the ending line number to highlight a range of lines.
Returns:
- None
Boto3 Documentation:
- aws codecommit post_comment_for_pull_request
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
"""
if self.boto_client is None:
self._connect_boto_client()
try:
# If the comment has code annotations,
# then set the file path and line number in the location dictionary
if annotation_file and annotation_line:
self.boto_client.post_comment_for_pull_request(
pullRequestId=str(pr_number),
repositoryName=repo_name,
beforeCommitId=destination_commit,
afterCommitId=source_commit,
content=comment,
location={
"filePath": annotation_file,
"filePosition": annotation_line,
"relativeFileVersion": "AFTER",
},
)
else:
# The comment does not have code annotations
self.boto_client.post_comment_for_pull_request( self.boto_client.post_comment_for_pull_request(
pullRequestId=str(pr_number), pullRequestId=str(pr_number),
repositoryName=repo_name, repositoryName=repo_name,

View File

@ -1,16 +1,16 @@
import logging
import os import os
import re
from collections import Counter from collections import Counter
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from ..algo.language_handler import is_valid_file, language_extension_map
from ..algo.pr_processing import clip_tokens
from ..algo.utils import load_large_diff
from ..config_loader import get_settings
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider, IncrementalPR
from pr_agent.git_providers.codecommit_client import CodeCommitClient from pr_agent.git_providers.codecommit_client import CodeCommitClient
from ..algo.utils import load_large_diff
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..config_loader import get_settings
from ..log import get_logger
class PullRequestCCMimic: class PullRequestCCMimic:
""" """
@ -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)
@ -73,6 +74,7 @@ class CodeCommitProvider(GitProvider):
"create_inline_comment", "create_inline_comment",
"publish_inline_comments", "publish_inline_comments",
"get_labels", "get_labels",
"gfm_markdown"
]: ]:
return False return False
return True return True
@ -153,26 +155,63 @@ class CodeCommitProvider(GitProvider):
return self.diff_files return self.diff_files
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
return "" # not implemented yet try:
self.codecommit_client.publish_description(
pr_number=self.pr_num,
pr_title=pr_title,
pr_body=CodeCommitProvider._add_additional_newlines(pr_body),
)
except Exception as e:
raise ValueError(f"CodeCommit Cannot publish description for PR: {self.pr_num}") from e
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if is_temporary: if is_temporary:
logging.info(pr_comment) get_logger().info(pr_comment)
return return
pr_comment = CodeCommitProvider._remove_markdown_html(pr_comment)
pr_comment = CodeCommitProvider._add_additional_newlines(pr_comment)
try: try:
self.codecommit_client.publish_comment( self.codecommit_client.publish_comment(
repo_name=self.repo_name, repo_name=self.repo_name,
pr_number=str(self.pr_num), pr_number=self.pr_num,
destination_commit=self.pr.destination_commit, destination_commit=self.pr.destination_commit,
source_commit=self.pr.source_commit, source_commit=self.pr.source_commit,
comment=pr_comment, comment=pr_comment,
) )
except Exception as e: except Exception as e:
raise ValueError(f"CodeCommit Cannot post comment for PR: {self.pr_num}") from e raise ValueError(f"CodeCommit Cannot publish comment for PR: {self.pr_num}") from e
def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_code_suggestions(self, code_suggestions: list) -> bool:
return [""] # not implemented yet counter = 1
for suggestion in code_suggestions:
# Verify that each suggestion has the required keys
if not all(key in suggestion for key in ["body", "relevant_file", "relevant_lines_start"]):
get_logger().warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys")
continue
# Publish the code suggestion to CodeCommit
try:
get_logger().debug(f"Code Suggestion #{counter} in file: {suggestion['relevant_file']}: {suggestion['relevant_lines_start']}")
self.codecommit_client.publish_comment(
repo_name=self.repo_name,
pr_number=self.pr_num,
destination_commit=self.pr.destination_commit,
source_commit=self.pr.source_commit,
comment=suggestion["body"],
annotation_file=suggestion["relevant_file"],
annotation_line=suggestion["relevant_lines_start"],
)
except Exception as e:
raise ValueError(f"CodeCommit Cannot publish code suggestions for PR: {self.pr_num}") from e
counter += 1
# The calling function passes in a list of code suggestions, and this function publishes each suggestion one at a time.
# If we were to return False here, the calling function will attempt to publish the same list of code suggestions again, one at a time.
# Since this function publishes the suggestions one at a time anyway, we always return True here to avoid the retry.
return True
def publish_labels(self, labels): def publish_labels(self, labels):
return [""] # not implemented yet return [""] # not implemented yet
@ -183,7 +222,11 @@ class CodeCommitProvider(GitProvider):
def remove_initial_comment(self): def remove_initial_comment(self):
return "" # not implemented yet return "" # not implemented yet
def remove_comment(self, comment):
return "" # not implemented yet
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
@ -193,14 +236,26 @@ class CodeCommitProvider(GitProvider):
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
def get_title(self): def get_title(self):
return self.pr.get("title", "") return self.pr.title
def get_pr_id(self):
"""
Returns the PR ID in the format: "repo_name/pr_number".
Note: This is an internal identifier for PR-Agent,
and is not the same as the CodeCommit PR identifier.
"""
try:
pr_id = f"{self.repo_name}/{self.pr_num}"
return pr_id
except:
return ""
def get_languages(self): def get_languages(self):
""" """
Returns a dictionary of languages, containing the percentage of each language used in the PR. Returns a dictionary of languages, containing the percentage of each language used in the PR.
Returns: Returns:
dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR. - dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
""" """
commit_files = self.get_files() commit_files = self.get_files()
filenames = [ item.filename for item in commit_files ] filenames = [ item.filename for item in commit_files ]
@ -215,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
@ -244,18 +301,29 @@ class CodeCommitProvider(GitProvider):
return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True) return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
get_logger().info("CodeCommit provider does not support eyes reaction yet")
return True return True
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
get_logger().info("CodeCommit provider does not support removing reactions yet")
return True return True
@staticmethod @staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]: def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
"""
Parse the CodeCommit PR URL and return the repository name and PR number.
Args:
- pr_url: the full AWS CodeCommit pull request URL
Returns:
- Tuple[str, int]: A tuple containing the repository name and PR number.
"""
# Example PR URL: # Example PR URL:
# https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456" # https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456"
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_url)
if "us-east-1.console.aws.amazon.com" not in parsed_url.netloc: if not CodeCommitProvider._is_valid_codecommit_hostname(parsed_url.netloc):
raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}") raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}")
path_parts = parsed_url.path.strip("/").split("/") path_parts = parsed_url.path.strip("/").split("/")
@ -278,17 +346,33 @@ class CodeCommitProvider(GitProvider):
return repo_name, pr_number return repo_name, pr_number
@staticmethod
def _is_valid_codecommit_hostname(hostname: str) -> bool:
"""
Check if the provided hostname is a valid AWS CodeCommit hostname.
This is not an exhaustive check of AWS region names,
but instead uses a regex to check for matching AWS region patterns.
Args:
- hostname: the hostname to check
Returns:
- bool: True if the hostname is valid, False otherwise.
"""
return re.match(r"^[a-z]{2}-(gov-)?[a-z]+-\d\.console\.aws\.amazon\.com$", hostname) is not None
def _get_pr(self): def _get_pr(self):
response = self.codecommit_client.get_pr(self.pr_num) response = self.codecommit_client.get_pr(self.repo_name, self.pr_num)
if len(response.targets) == 0: if len(response.targets) == 0:
raise ValueError(f"No files found in CodeCommit PR: {self.pr_num}") raise ValueError(f"No files found in CodeCommit PR: {self.pr_num}")
# TODO: implement support for multiple commits in one CodeCommit PR # TODO: implement support for multiple targets in one CodeCommit PR
# for now, we are only using the first commit in the PR # for now, we are only using the first target in the PR
if len(response.targets) > 1: if len(response.targets) > 1:
logging.warning( get_logger().warning(
"Multiple commits in one PR is not supported for CodeCommit yet. Continuing, using the first commit only..." "Multiple targets in one PR is not supported for CodeCommit yet. Continuing, using the first target only..."
) )
# Return our object that mimics PullRequest class from the PyGithub library # Return our object that mimics PullRequest class from the PyGithub library
@ -306,13 +390,52 @@ class CodeCommitProvider(GitProvider):
return "" # not implemented yet return "" # not implemented yet
@staticmethod @staticmethod
def _get_edit_type(codecommit_change_type): def _add_additional_newlines(body: str) -> str:
"""
Replace single newlines in a PR body with double newlines.
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
so we add additional newlines to the PR body to make it more readable in CodeCommit.
Args:
- body: the PR body
Returns:
- str: the PR body with the double newlines added
"""
return re.sub(r'(?<!\n)\n(?!\n)', '\n\n', body)
@staticmethod
def _remove_markdown_html(comment: str) -> str:
"""
Remove the HTML tags from a PR comment.
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
so we remove the HTML tags from the PR comment to make it more readable in CodeCommit.
Args:
- comment: the PR comment
Returns:
- str: the PR comment with the HTML tags removed
"""
comment = comment.replace("<details>", "")
comment = comment.replace("</details>", "")
comment = comment.replace("<summary>", "")
comment = comment.replace("</summary>", "")
return comment
@staticmethod
def _get_edit_type(codecommit_change_type: str):
""" """
Convert the CodeCommit change type string to the EDIT_TYPE enum. Convert the CodeCommit change type string to the EDIT_TYPE enum.
The CodeCommit change type string is returned from the get_differences SDK method. The CodeCommit change type string is returned from the get_differences SDK method.
Args:
- codecommit_change_type: the CodeCommit change type string
Returns: Returns:
An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff. - An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff.
""" """
t = codecommit_change_type.upper() t = codecommit_change_type.upper()
edit_type = None edit_type = None
@ -333,6 +456,12 @@ class CodeCommitProvider(GitProvider):
The returned extensions will include the dot "." prefix, The returned extensions will include the dot "." prefix,
to accommodate for the dots in the existing language_extension_map settings. to accommodate for the dots in the existing language_extension_map settings.
Filenames with no extension will return an empty string for the extension. Filenames with no extension will return an empty string for the extension.
Args:
- filenames: a list of filenames
Returns:
- list: A list of file extensions, including the dot "." prefix.
""" """
extensions = [] extensions = []
for filename in filenames: for filename in filenames:
@ -349,6 +478,12 @@ class CodeCommitProvider(GitProvider):
Return a dictionary containing the programming language name (as the key), Return a dictionary containing the programming language name (as the key),
and the percentage that language is used (as the value), and the percentage that language is used (as the value),
given a list of file extensions. given a list of file extensions.
Args:
- extensions: a list of file extensions
Returns:
- dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
""" """
total_files = len(extensions) total_files = len(extensions)
if total_files == 0: if total_files == 0:

View File

@ -0,0 +1,403 @@
import json
import os
import pathlib
import shutil
import subprocess
import uuid
from collections import Counter, namedtuple
from pathlib import Path
from tempfile import NamedTemporaryFile, mkdtemp
import requests
import urllib3.util
from git import Repo
from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from pr_agent.git_providers.local_git_provider import PullRequestMimic
from pr_agent.log import get_logger
def _call(*command, **kwargs) -> (int, str, str):
res = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
**kwargs,
)
return res.stdout.decode()
def clone(url, directory):
get_logger().info("Cloning %s to %s", url, directory)
stdout = _call('git', 'clone', "--depth", "1", url, directory)
get_logger().info(stdout)
def fetch(url, refspec, cwd):
get_logger().info("Fetching %s %s", url, refspec)
stdout = _call(
'git', 'fetch', '--depth', '2', url, refspec,
cwd=cwd
)
get_logger().info(stdout)
def checkout(cwd):
get_logger().info("Checking out")
stdout = _call('git', 'checkout', "FETCH_HEAD", cwd=cwd)
get_logger().info(stdout)
def show(*args, cwd=None):
get_logger().info("Show")
return _call('git', 'show', *args, cwd=cwd)
def diff(*args, cwd=None):
get_logger().info("Diff")
patch = _call('git', 'diff', *args, cwd=cwd)
if not patch:
get_logger().warning("No changes found")
return
return patch
def reset_local_changes(cwd):
get_logger().info("Reset local changes")
_call('git', 'checkout', "--force", cwd=cwd)
def add_comment(url: urllib3.util.Url, refspec, message):
*_, patchset, changenum = refspec.rsplit("/")
message = "'" + message.replace("'", "'\"'\"'") + "'"
return _call(
"ssh",
"-p", str(url.port),
f"{url.auth}@{url.host}",
"gerrit", "review",
"--message", message,
# "--code-review", score,
f"{patchset},{changenum}",
)
def list_comments(url: urllib3.util.Url, refspec):
*_, patchset, _ = refspec.rsplit("/")
stdout = _call(
"ssh",
"-p", str(url.port),
f"{url.auth}@{url.host}",
"gerrit", "query",
"--comments",
"--current-patch-set", patchset,
"--format", "JSON",
)
change_set, *_ = stdout.splitlines()
return json.loads(change_set)["currentPatchSet"]["comments"]
def prepare_repo(url: urllib3.util.Url, project, refspec):
repo_url = (f"{url.scheme}://{url.auth}@{url.host}:{url.port}/{project}")
directory = pathlib.Path(mkdtemp())
clone(repo_url, directory),
fetch(repo_url, refspec, cwd=directory)
checkout(cwd=directory)
return directory
def adopt_to_gerrit_message(message):
lines = message.splitlines()
buf = []
for line in lines:
# remove markdown formatting
line = (line.replace("*", "")
.replace("``", "`")
.replace("<details>", "")
.replace("</details>", "")
.replace("<summary>", "")
.replace("</summary>", ""))
line = line.strip()
if line.startswith('#'):
buf.append("\n" +
line.replace('#', '').removesuffix(":").strip() +
":")
continue
elif line.startswith('-'):
buf.append(line.removeprefix('-').strip())
continue
else:
buf.append(line)
return "\n".join(buf).strip()
def add_suggestion(src_filename, context: str, start, end: int):
with (
NamedTemporaryFile("w", delete=False) as tmp,
open(src_filename, "r") as src
):
lines = src.readlines()
tmp.writelines(lines[:start - 1])
if context:
tmp.write(context)
tmp.writelines(lines[end:])
shutil.copy(tmp.name, src_filename)
os.remove(tmp.name)
def upload_patch(patch, path):
patch_server_endpoint = get_settings().get(
'gerrit.patch_server_endpoint')
patch_server_token = get_settings().get(
'gerrit.patch_server_token')
response = requests.post(
patch_server_endpoint,
json={
"content": patch,
"path": path,
},
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {patch_server_token}",
}
)
response.raise_for_status()
patch_server_endpoint = patch_server_endpoint.rstrip("/")
return patch_server_endpoint + "/" + path
class GerritProvider(GitProvider):
def __init__(self, key: str, incremental=False):
self.project, self.refspec = key.split(':')
assert self.project, "Project name is required"
assert self.refspec, "Refspec is required"
base_url = get_settings().get('gerrit.url')
assert base_url, "Gerrit URL is required"
user = get_settings().get('gerrit.user')
assert user, "Gerrit user is required"
parsed = urllib3.util.parse_url(base_url)
self.parsed_url = urllib3.util.parse_url(
f"{parsed.scheme}://{user}@{parsed.host}:{parsed.port}"
)
self.repo_path = prepare_repo(
self.parsed_url, self.project, self.refspec
)
self.repo = Repo(self.repo_path)
assert self.repo
self.pr_url = base_url
self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files())
def get_pr_title(self):
"""
Substitutes the branch-name as the PR-mimic title.
"""
return self.repo.branches[0].name
def get_issue_comments(self):
comments = list_comments(self.parsed_url, self.refspec)
Comments = namedtuple('Comments', ['reversed'])
Comment = namedtuple('Comment', ['body'])
return Comments([Comment(c['message']) for c in reversed(comments)])
def get_labels(self):
raise NotImplementedError(
'Getting labels is not implemented for the gerrit provider')
def add_eyes_reaction(self, issue_comment_id: int):
raise NotImplementedError(
'Adding reactions is not implemented for the gerrit provider')
def remove_reaction(self, issue_comment_id: int, reaction_id: int):
raise NotImplementedError(
'Removing reactions is not implemented for the gerrit provider')
def get_commit_messages(self):
return [self.repo.head.commit.message]
def get_repo_settings(self):
try:
with open(self.repo_path / ".pr_agent.toml", 'rb') as f:
contents = f.read()
return contents
except OSError:
return b""
def get_diff_files(self) -> list[FilePatchInfo]:
diffs = self.repo.head.commit.diff(
self.repo.head.commit.parents[0], # previous commit
create_patch=True,
R=True
)
diff_files = []
for diff_item in diffs:
if diff_item.a_blob is not None:
original_file_content_str = (
diff_item.a_blob.data_stream.read().decode('utf-8')
)
else:
original_file_content_str = "" # empty file
if diff_item.b_blob is not None:
new_file_content_str = diff_item.b_blob.data_stream.read(). \
decode('utf-8')
else:
new_file_content_str = "" # empty file
edit_type = EDIT_TYPE.MODIFIED
if diff_item.new_file:
edit_type = EDIT_TYPE.ADDED
elif diff_item.deleted_file:
edit_type = EDIT_TYPE.DELETED
elif diff_item.renamed_file:
edit_type = EDIT_TYPE.RENAMED
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
diff_item.diff.decode('utf-8'),
diff_item.b_path,
edit_type=edit_type,
old_filename=None
if diff_item.a_path == diff_item.b_path
else diff_item.a_path
)
)
self.diff_files = diff_files
return diff_files
def get_files(self):
diff_index = self.repo.head.commit.diff(
self.repo.head.commit.parents[0], # previous commit
R=True
)
# Get the list of changed files
diff_files = [item.a_path for item in diff_index]
return diff_files
def get_languages(self):
"""
Calculate percentage of languages in repository. Used for hunk
prioritisation.
"""
# Get all files in repository
filepaths = [Path(item.path) for item in
self.repo.tree().traverse() if item.type == 'blob']
# Identify language by file extension and count
lang_count = Counter(
ext.lstrip('.') for filepath in filepaths for ext in
[filepath.suffix.lower()])
# Convert counts to percentages
total_files = len(filepaths)
lang_percentage = {lang: count / total_files * 100 for lang, count
in lang_count.items()}
return lang_percentage
def get_pr_description_full(self):
return self.repo.head.commit.message
def get_user_id(self):
return self.repo.head.commit.author.email
def is_supported(self, capability: str) -> bool:
if capability in [
# 'get_issue_comments',
'create_inline_comment',
'publish_inline_comments',
'get_labels',
'gfm_markdown'
]:
return False
return True
def split_suggestion(self, msg) -> tuple[str, str]:
is_code_context = False
description = []
context = []
for line in msg.splitlines():
if line.startswith('```suggestion'):
is_code_context = True
continue
if line.startswith('```'):
is_code_context = False
continue
if is_code_context:
context.append(line)
else:
description.append(
line.replace('*', '')
)
return (
'\n'.join(description),
'\n'.join(context) + '\n' if context else ''
)
def publish_code_suggestions(self, code_suggestions: list):
msg = []
for suggestion in code_suggestions:
description, code = self.split_suggestion(suggestion['body'])
add_suggestion(
pathlib.Path(self.repo_path) / suggestion["relevant_file"],
code,
suggestion["relevant_lines_start"],
suggestion["relevant_lines_end"],
)
patch = diff(cwd=self.repo_path)
patch_id = uuid.uuid4().hex[0:4]
path = "/".join(["codium-ai", self.refspec, patch_id])
full_path = upload_patch(patch, path)
reset_local_changes(self.repo_path)
msg.append(f'* {description}\n{full_path}')
if msg:
add_comment(self.parsed_url, self.refspec, "\n".join(msg))
return True
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if not is_temporary:
msg = adopt_to_gerrit_message(pr_comment)
add_comment(self.parsed_url, self.refspec, msg)
def publish_description(self, pr_title: str, pr_body: str):
msg = adopt_to_gerrit_message(pr_body)
add_comment(self.parsed_url, self.refspec, pr_title + '\n' + msg)
def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError(
'Publishing inline comments is not implemented for the gerrit '
'provider')
def publish_inline_comment(self, body: str, relevant_file: str,
relevant_line_in_file: str):
raise NotImplementedError(
'Publishing inline comments is not implemented for the gerrit '
'provider')
def create_inline_comment(self, body: str, relevant_file: str,
relevant_line_in_file: str):
raise NotImplementedError(
'Creating inline comments is not implemented for the gerrit '
'provider')
def publish_labels(self, labels):
# Not applicable to the local git provider,
# but required by the interface
pass
def remove_initial_comment(self):
# remove repo, cloned in previous steps
# shutil.rmtree(self.repo_path)
pass
def remove_comment(self, comment):
pass
def get_pr_branch(self):
return self.repo.head

View File

@ -1,4 +1,3 @@
import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
@ -6,12 +5,16 @@ 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
class EDIT_TYPE(Enum): class EDIT_TYPE(Enum):
ADDED = 1 ADDED = 1
DELETED = 2 DELETED = 2
MODIFIED = 3 MODIFIED = 3
RENAMED = 4 RENAMED = 4
UNKNOWN = 5
@dataclass @dataclass
@ -21,8 +24,10 @@ class FilePatchInfo:
patch: str patch: str
filename: str filename: str
tokens: int = -1 tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.MODIFIED 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):
@ -38,38 +43,10 @@ class GitProvider(ABC):
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
pass pass
@abstractmethod
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
pass
@abstractmethod
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@abstractmethod
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@abstractmethod
def publish_inline_comments(self, comments: list[dict]):
pass
@abstractmethod @abstractmethod
def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_code_suggestions(self, code_suggestions: list) -> bool:
pass pass
@abstractmethod
def publish_labels(self, labels):
pass
@abstractmethod
def get_labels(self):
pass
@abstractmethod
def remove_initial_comment(self):
pass
@abstractmethod @abstractmethod
def get_languages(self): def get_languages(self):
pass pass
@ -86,19 +63,19 @@ class GitProvider(ABC):
def get_pr_description_full(self) -> str: def get_pr_description_full(self) -> str:
pass pass
def get_pr_description(self) -> 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 = 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() description = self.get_pr_description_full() if full else self.get_user_description()
if max_tokens: if max_tokens_description:
return clip_tokens(description, max_tokens) return clip_tokens(description, max_tokens_description)
return description return description
def get_user_description(self) -> str: def get_user_description(self) -> str:
description = (self.get_pr_description_full() or "").strip() description = (self.get_pr_description_full() or "").strip()
# if the existing description wasn't generated by the pr-agent, just return it as-is # if the existing description wasn't generated by the pr-agent, just return it as-is
if not description.startswith("## PR Type"): if not any(description.startswith(header) for header in ("## PR Type", "## PR Description")):
return description return description
# if the existing description was generated by the pr-agent, but it doesn't contain the user description, # if the existing description was generated by the pr-agent, but it doesn't contain the user description,
# return nothing (empty string) because it means there is no user description # return nothing (empty string) because it means there is no user description
@ -108,11 +85,57 @@ class GitProvider(ABC):
return description.split("## User Description:", 1)[1].strip() return description.split("## User Description:", 1)[1].strip()
@abstractmethod @abstractmethod
def get_issue_comments(self): def get_repo_settings(self):
pass
def get_pr_id(self):
return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
return ""
#### comments operations ####
@abstractmethod
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
pass
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool):
self.publish_comment(pr_comment)
@abstractmethod
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass pass
@abstractmethod @abstractmethod
def get_repo_settings(self): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@abstractmethod
def publish_inline_comments(self, comments: list[dict]):
pass
@abstractmethod
def remove_initial_comment(self):
pass
@abstractmethod
def remove_comment(self, comment):
pass
@abstractmethod
def get_issue_comments(self):
pass
def get_comment_url(self, comment) -> str:
return ""
#### labels operations ####
@abstractmethod
def publish_labels(self, labels):
pass
@abstractmethod
def get_labels(self):
pass pass
@abstractmethod @abstractmethod
@ -123,50 +146,78 @@ class GitProvider(ABC):
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
pass pass
#### commits operations ####
@abstractmethod @abstractmethod
def get_commit_messages(self): def get_commit_messages(self):
pass pass
def get_latest_commit_url(self) -> str:
return ""
def get_main_pr_language(languages, files) -> str: def get_main_pr_language(languages, files) -> str:
""" """
Get the main language of the commit. Return an empty string if cannot determine. Get the main language of the commit. Return an empty string if cannot determine.
""" """
main_language_str = "" main_language_str = ""
if not languages:
get_logger().info("No languages detected")
return main_language_str
if not files:
get_logger().info("No files in diff")
return main_language_str
try: try:
top_language = max(languages, key=languages.get).lower() top_language = max(languages, key=languages.get).lower()
# validate that the specific commit uses the main language # validate that the specific commit uses the main language
extension_list = [] extension_list = []
for file in files: for file in files:
if not file:
continue
if isinstance(file, str): if isinstance(file, str):
file = FilePatchInfo(base_file=None, head_file=None, patch=None, filename=file) file = FilePatchInfo(base_file=None, head_file=None, patch=None, filename=file)
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 \
most_common_extension == 'js' and top_language == 'javascript' or \
most_common_extension == 'ts' and top_language == 'typescript' or \
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 == 'swift' and top_language == 'swift' or \
most_common_extension == top_language:
main_language_str = top_language main_language_str = top_language
else:
for language, extensions in language_extension_map.items():
if most_common_extension in extensions:
main_language_str = language
break
except Exception as e:
get_logger().exception(f"Failed to get main language: {e}")
pass
## old approach:
# most_common_extension = max(set(extension_list), key=extension_list.count)
# if most_common_extension == 'py' and top_language == 'python' or \
# most_common_extension == 'js' and top_language == 'javascript' or \
# most_common_extension == 'ts' and top_language == 'typescript' or \
# most_common_extension == 'tsx' and top_language == 'typescript' or \
# 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:
logging.exception(e) get_logger().exception(e)
pass pass
return main_language_str return main_language_str
@ -176,6 +227,13 @@ class IncrementalPR:
def __init__(self, is_incremental: bool = False): def __init__(self, is_incremental: bool = False):
self.is_incremental = is_incremental self.is_incremental = is_incremental
self.commits_range = None self.commits_range = None
self.first_new_commit_sha = None self.first_new_commit = None
self.last_seen_commit_sha = None self.last_seen_commit = None
@property
def first_new_commit_sha(self):
return None if self.first_new_commit is None else self.first_new_commit.sha
@property
def last_seen_commit_sha(self):
return None if self.last_seen_commit is None else self.last_seen_commit.sha

View File

@ -1,20 +1,19 @@
import logging
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from typing import Optional, Tuple, Any from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from github import AppAuthentication, Auth, Github, GithubException, Reaction from github import AppAuthentication, Auth, Github, GithubException
from retry import retry from retry import retry
from starlette_context import context from starlette_context import context
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.utils import load_large_diff from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file, clip_tokens 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 ..servers.utils import RateLimitExceeded from ..servers.utils import RateLimitExceeded
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR, EDIT_TYPE
class GithubProvider(GitProvider): class GithubProvider(GitProvider):
@ -32,9 +31,10 @@ class GithubProvider(GitProvider):
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.incremental = incremental self.incremental = incremental
if 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
@ -51,36 +51,44 @@ class GithubProvider(GitProvider):
def get_incremental_commits(self): def get_incremental_commits(self):
self.commits = list(self.pr.get_commits()) self.commits = list(self.pr.get_commits())
self.get_previous_review() self.previous_review = self.get_previous_review(full=True, incremental=True)
if self.previous_review: if self.previous_review:
self.incremental.commits_range = self.get_commit_range() self.incremental.commits_range = self.get_commit_range()
# Get all files changed during the commit range # Get all files changed during the commit range
self.file_set = dict() self.file_set = dict()
for commit in self.incremental.commits_range: for commit in self.incremental.commits_range:
if commit.commit.message.startswith(f"Merge branch '{self._get_repo().default_branch}'"): if commit.commit.message.startswith(f"Merge branch '{self._get_repo().default_branch}'"):
logging.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
first_new_commit_index = 0 first_new_commit_index = None
for index in range(len(self.commits) - 1, -1, -1): for index in range(len(self.commits) - 1, -1, -1):
if self.commits[index].commit.author.date > last_review_time: if self.commits[index].commit.author.date > last_review_time:
self.incremental.first_new_commit_sha = self.commits[index].sha self.incremental.first_new_commit = self.commits[index]
first_new_commit_index = index first_new_commit_index = index
else: else:
self.incremental.last_seen_commit_sha = self.commits[index].sha self.incremental.last_seen_commit = self.commits[index]
break break
return self.commits[first_new_commit_index:] return self.commits[first_new_commit_index:] if first_new_commit_index is not None else []
def get_previous_review(self): def get_previous_review(self, *, full: bool, incremental: bool):
self.previous_review = None if not (full or incremental):
raise ValueError("At least one of full or incremental must be True")
if not getattr(self, "comments", None):
self.comments = list(self.pr.get_issue_comments()) self.comments = list(self.pr.get_issue_comments())
prefixes = []
if full:
prefixes.append("## PR Analysis")
if incremental:
prefixes.append("## Incremental PR Review")
for index in range(len(self.comments) - 1, -1, -1): for index in range(len(self.comments) - 1, -1, -1):
if self.comments[index].body.startswith("## PR Analysis"): if any(self.comments[index].body.startswith(prefix) for prefix in prefixes):
self.previous_review = self.comments[index] return self.comments[index]
break
def get_files(self): def get_files(self):
if self.incremental.is_incremental and self.file_set: if self.incremental.is_incremental and self.file_set:
@ -124,22 +132,68 @@ class GithubProvider(GitProvider):
if not patch: if not patch:
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str) patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, patch, file.filename)) if file.status == 'added':
edit_type = EDIT_TYPE.ADDED
elif file.status == 'removed':
edit_type = EDIT_TYPE.DELETED
elif file.status == 'renamed':
edit_type = EDIT_TYPE.RENAMED
elif file.status == 'modified':
edit_type = EDIT_TYPE.MODIFIED
else:
get_logger().error(f"Unknown edit type: {file.status}")
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.filename, edit_type=edit_type,
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines,)
diff_files.append(file_patch_canonical_structure)
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
except GithubException.RateLimitExceededException as e: except GithubException.RateLimitExceededException as e:
logging.error(f"Rate limit exceeded for GitHub API. Original message: {e}") get_logger().error(f"Rate limit exceeded for GitHub API. Original message: {e}")
raise RateLimitExceeded("Rate limit exceeded for GitHub API.") from e raise RateLimitExceeded("Rate limit exceeded for GitHub API.") from e
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
self.pr.edit(title=pr_title, body=pr_body) self.pr.edit(title=pr_title, body=pr_body)
def get_latest_commit_url(self) -> str:
return self.last_commit_id.html_url
def get_comment_url(self, comment) -> str:
return comment.html_url
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
prev_comments = list(self.pr.get_issue_comments())
for comment in prev_comments:
body = comment.body
if body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
response = comment.edit(pr_comment_updated)
self.publish_comment(
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
return
self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if is_temporary and not get_settings().config.publish_output_progress: if is_temporary and not get_settings().config.publish_output_progress:
logging.debug(f"Skipping publish_comment for temporary comment: {pr_comment}") get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
return return
response = self.pr.create_issue_comment(pr_comment) response = self.pr.create_issue_comment(pr_comment)
if hasattr(response, "user") and hasattr(response.user, "login"): if hasattr(response, "user") and hasattr(response.user, "login"):
self.github_user_id = response.user.login self.github_user_id = response.user.login
@ -156,7 +210,7 @@ class GithubProvider(GitProvider):
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file) position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
subject_type = "FILE" subject_type = "FILE"
else: else:
subject_type = "LINE" subject_type = "LINE"
@ -179,13 +233,13 @@ class GithubProvider(GitProvider):
if not relevant_lines_start or relevant_lines_start == -1: if not relevant_lines_start or relevant_lines_start == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.exception( get_logger().exception(
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}") f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
continue continue
if relevant_lines_end < relevant_lines_start: if relevant_lines_end < relevant_lines_start:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.exception(f"Failed to publish code suggestion, " get_logger().exception(f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and " f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}") f"relevant_lines_start is {relevant_lines_start}")
continue continue
@ -212,16 +266,22 @@ class GithubProvider(GitProvider):
return True return True
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.error(f"Failed to publish code suggestion, error: {e}") get_logger().error(f"Failed to publish code suggestion, error: {e}")
return False return False
def remove_initial_comment(self): def remove_initial_comment(self):
try: try:
for comment in getattr(self.pr, 'comments_list', []): for comment in getattr(self.pr, 'comments_list', []):
if comment.is_temporary: if comment.is_temporary:
self.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove initial comment, error: {e}")
def remove_comment(self, comment):
try:
comment.delete() comment.delete()
except Exception as e: except Exception as e:
logging.exception(f"Failed to remove initial comment, error: {e}") get_logger().exception(f"Failed to remove comment, error: {e}")
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
@ -239,9 +299,10 @@ class GithubProvider(GitProvider):
def get_user_id(self): def get_user_id(self):
if not self.github_user_id: if not self.github_user_id:
try: try:
self.github_user_id = self.github_client.get_user().login self.github_user_id = self.github_client.get_user().raw_data['login']
except Exception as e: except Exception as e:
logging.exception(f"Failed to get user id, error: {e}") self.github_user_id = ""
# logging.exception(f"Failed to get user id, error: {e}")
return self.github_user_id return self.github_user_id
def get_notifications(self, since: datetime): def get_notifications(self, since: datetime):
@ -258,7 +319,10 @@ class GithubProvider(GitProvider):
def get_repo_settings(self): def get_repo_settings(self):
try: try:
contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content # contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
# more logical to take 'pr_agent.toml' from the default branch
contents = self.repo_obj.get_contents(".pr_agent.toml").decoded_content
return contents return contents
except Exception: except Exception:
return "" return ""
@ -268,7 +332,7 @@ class GithubProvider(GitProvider):
reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes") reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes")
return reaction.id return reaction.id
except Exception as e: except Exception as e:
logging.exception(f"Failed to add eyes reaction, error: {e}") get_logger().exception(f"Failed to add eyes reaction, error: {e}")
return None return None
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
@ -276,7 +340,7 @@ class GithubProvider(GitProvider):
self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id) self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id)
return True return True
except Exception as e: except Exception as e:
logging.exception(f"Failed to remove eyes reaction, error: {e}") get_logger().exception(f"Failed to remove eyes reaction, error: {e}")
return False return False
@ -309,6 +373,35 @@ class GithubProvider(GitProvider):
return repo_name, pr_number return repo_name, pr_number
@staticmethod
def _parse_issue_url(issue_url: str) -> Tuple[str, int]:
parsed_url = urlparse(issue_url)
if 'github.com' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid GitHub URL")
path_parts = parsed_url.path.strip('/').split('/')
if 'api.github.com' in parsed_url.netloc:
if len(path_parts) < 5 or path_parts[3] != 'issues':
raise ValueError("The provided URL does not appear to be a GitHub ISSUE URL")
repo_name = '/'.join(path_parts[1:3])
try:
issue_number = int(path_parts[4])
except ValueError as e:
raise ValueError("Unable to convert issue number to integer") from e
return repo_name, issue_number
if len(path_parts) < 4 or path_parts[2] != 'issues':
raise ValueError("The provided URL does not appear to be a GitHub PR issue")
repo_name = '/'.join(path_parts[:2])
try:
issue_number = int(path_parts[3])
except ValueError as e:
raise ValueError("Unable to convert issue number to integer") from e
return repo_name, issue_number
def _get_github_client(self): def _get_github_client(self):
deployment_type = get_settings().get("GITHUB.DEPLOYMENT_TYPE", "user") deployment_type = get_settings().get("GITHUB.DEPLOYMENT_TYPE", "user")
@ -322,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:
@ -331,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 \
@ -356,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:
@ -366,13 +459,13 @@ class GithubProvider(GitProvider):
"PUT", f"{self.pr.issue_url}/labels", input=post_parameters "PUT", f"{self.pr.issue_url}/labels", input=post_parameters
) )
except Exception as e: except Exception as e:
logging.exception(f"Failed to publish labels, error: {e}") get_logger().exception(f"Failed to publish labels, error: {e}")
def get_labels(self): def get_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:
logging.exception(f"Failed to get labels, error: {e}") get_logger().exception(f"Failed to get labels, error: {e}")
return [] return []
def get_commit_messages(self): def get_commit_messages(self):
@ -414,6 +507,24 @@ class GithubProvider(GitProvider):
return link return link
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Failed adding line link, error: {e}") get_logger().info(f"Failed adding line link, error: {e}")
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):
try:
pr_id = f"{self.repo}/{self.pr_num}"
return pr_id
except:
return ""

View File

@ -1,4 +1,4 @@
import logging import hashlib
import re import re
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
@ -7,12 +7,12 @@ 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 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
logger = logging.getLogger()
class DiffNotFoundError(Exception): class DiffNotFoundError(Exception):
"""Raised when the diff for a merge request cannot be found.""" """Raised when the diff for a merge request cannot be found."""
@ -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']: 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
@ -58,7 +59,7 @@ class GitLabProvider(GitProvider):
try: try:
self.last_diff = self.mr.diffs.list(get_all=True)[-1] self.last_diff = self.mr.diffs.list(get_all=True)[-1]
except IndexError as e: except IndexError as e:
logger.error(f"Could not get diff for merge request {self.id_mr}") get_logger().error(f"Could not get diff for merge request {self.id_mr}")
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e
@ -98,7 +99,7 @@ class GitLabProvider(GitProvider):
if isinstance(new_file_content_str, bytes): if isinstance(new_file_content_str, bytes):
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8') new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
logging.warning( get_logger().warning(
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}") f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
edit_type = EDIT_TYPE.MODIFIED edit_type = EDIT_TYPE.MODIFIED
@ -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
@ -134,7 +143,34 @@ class GitLabProvider(GitProvider):
self.mr.description = pr_body self.mr.description = pr_body
self.mr.save() self.mr.save()
except Exception as e: except Exception as e:
logging.exception(f"Could not update merge request {self.id_mr} description: {e}") get_logger().exception(f"Could not update merge request {self.id_mr} description: {e}")
def get_latest_commit_url(self):
return self.mr.commits().next().web_url
def get_comment_url(self, comment):
return f"{self.mr.web_url}#note_{comment.id}"
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
try:
for comment in self.mr.notes.list(get_all=True)[::-1]:
if comment.body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
self.publish_comment(
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
def publish_comment(self, mr_comment: str, is_temporary: bool = False): def publish_comment(self, mr_comment: str, is_temporary: bool = False):
comment = self.mr.notes.create({'body': mr_comment}) comment = self.mr.notes.create({'body': mr_comment})
@ -156,12 +192,12 @@ class GitLabProvider(GitProvider):
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int, def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
source_line_no: int, target_file: str,target_line_no: int) -> None: source_line_no: int, target_file: str,target_line_no: int) -> None:
if not found: if not found:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
else: else:
# in order to have exact sha's we have to find correct diff for this change # in order to have exact sha's we have to find correct diff for this change
diff = self.get_relevant_diff(relevant_file, relevant_line_in_file) diff = self.get_relevant_diff(relevant_file, relevant_line_in_file)
if diff is None: if diff is None:
logger.error(f"Could not get diff for merge request {self.id_mr}") get_logger().error(f"Could not get diff for merge request {self.id_mr}")
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}")
pos_obj = {'position_type': 'text', pos_obj = {'position_type': 'text',
'new_path': target_file.filename, 'new_path': target_file.filename,
@ -174,24 +210,23 @@ class GitLabProvider(GitProvider):
else: else:
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
logging.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, self.mr.discussions.create({'body': body, 'position': pos_obj})
'position': pos_obj})
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: 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
if not changes: if not changes:
logging.error('No changes found for the merge request.') get_logger().error('No changes found for the merge request.')
return None return None
all_diffs = self.mr.diffs.list(get_all=True) all_diffs = self.mr.diffs.list(get_all=True)
if not all_diffs: if not all_diffs:
logging.error('No diffs found for the merge request.') get_logger().error('No diffs found for the merge request.')
return None return None
for diff in all_diffs: for diff in all_diffs:
for change in changes['changes']: for change in changes['changes']:
if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']: if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']:
return diff return diff
logging.debug( get_logger().debug(
f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.') f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.')
return self.last_diff # fallback to last_diff if no relevant diff is found return self.last_diff # fallback to last_diff if no relevant diff is found
@ -226,7 +261,10 @@ class GitLabProvider(GitProvider):
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no) target_file, target_line_no)
except Exception as e: except Exception as e:
logging.exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") get_logger().exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}")
# note that we publish suggestions one-by-one. so, if one fails, the rest will still be published
return True
def search_line(self, relevant_file, relevant_line_in_file): def search_line(self, relevant_file, relevant_line_in_file):
target_file = None target_file = None
@ -285,9 +323,15 @@ class GitLabProvider(GitProvider):
def remove_initial_comment(self): def remove_initial_comment(self):
try: try:
for comment in self.temp_comments: for comment in self.temp_comments:
self.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove temp comments, error: {e}")
def remove_comment(self, comment):
try:
comment.delete() comment.delete()
except Exception as e: except Exception as e:
logging.exception(f"Failed to remove temp comments, error: {e}") get_logger().exception(f"Failed to remove comment, error: {e}")
def get_title(self): def get_title(self):
return self.mr.title return self.mr.title
@ -307,7 +351,7 @@ class GitLabProvider(GitProvider):
def get_repo_settings(self): def get_repo_settings(self):
try: try:
contents = self.gl.projects.get(self.id_project).files.get(file_path='.pr_agent.toml', ref=self.mr.source_branch) contents = self.gl.projects.get(self.id_project).files.get(file_path='.pr_agent.toml', ref=self.mr.target_branch).decode()
return contents return contents
except Exception: except Exception:
return "" return ""
@ -355,7 +399,7 @@ class GitLabProvider(GitProvider):
self.mr.labels = list(set(pr_types)) self.mr.labels = list(set(pr_types))
self.mr.save() self.mr.save()
except Exception as e: except Exception as e:
logging.exception(f"Failed to publish labels, error: {e}") get_logger().exception(f"Failed to publish labels, error: {e}")
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
pass pass
@ -379,3 +423,44 @@ class GitLabProvider(GitProvider):
if max_tokens: if max_tokens:
commit_messages_str = clip_tokens(commit_messages_str, max_tokens) commit_messages_str = clip_tokens(commit_messages_str, max_tokens)
return commit_messages_str return commit_messages_str
def get_pr_id(self):
try:
pr_id = self.mr.web_url
return pr_id
except:
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:
try:
relevant_file = suggestion['relevant file'].strip('`').strip("'")
relevant_line_str = suggestion['relevant line']
if not relevant_line_str:
return ""
position, absolute_position = find_line_number_of_relevant_line_in_file \
(self.diff_files, relevant_file, relevant_line_str)
if absolute_position != -1:
# link to right file only
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{absolute_position}"
# # link to diff
# sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest()
# link = f"{self.pr.web_url}/diffs#{sha_file}_{absolute_position}_{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 ""

View File

@ -1,4 +1,3 @@
import logging
from collections import Counter from collections import Counter
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@ -7,6 +6,7 @@ from git import Repo
from pr_agent.config_loader import _find_repository_root, get_settings from pr_agent.config_loader import _find_repository_root, get_settings
from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from pr_agent.log import get_logger
class PullRequestMimic: class PullRequestMimic:
@ -49,14 +49,15 @@ class LocalGitProvider(GitProvider):
""" """
Prepare the repository for PR-mimic generation. Prepare the repository for PR-mimic generation.
""" """
logging.debug('Preparing repository for PR-mimic generation...') get_logger().debug('Preparing repository for PR-mimic generation...')
if self.repo.is_dirty(): if self.repo.is_dirty():
raise ValueError('The repository is not in a clean state. Please commit or stash pending changes.') raise ValueError('The repository is not in a clean state. Please commit or stash pending changes.')
if self.target_branch_name not in self.repo.heads: if self.target_branch_name not in self.repo.heads:
raise KeyError(f'Branch: {self.target_branch_name} does not exist') raise KeyError(f'Branch: {self.target_branch_name} does not exist')
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels',
'gfm_markdown']:
return False return False
return True return True
@ -139,6 +140,9 @@ class LocalGitProvider(GitProvider):
def remove_initial_comment(self): def remove_initial_comment(self):
pass # Not applicable to the local git provider, but required by the interface pass # Not applicable to the local git provider, but required by the interface
def remove_comment(self, comment):
pass # Not applicable to the local git provider, but required by the interface
def get_languages(self): def get_languages(self):
""" """
Calculate percentage of languages in repository. Used for hunk prioritisation. Calculate percentage of languages in repository. Used for hunk prioritisation.

View File

@ -0,0 +1,37 @@
import copy
import os
import tempfile
from dynaconf import Dynaconf
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.log import get_logger
def apply_repo_settings(pr_url):
if get_settings().config.use_repo_settings_file:
repo_settings_file = None
try:
git_provider = get_git_provider()(pr_url)
repo_settings = git_provider.get_repo_settings()
if repo_settings:
repo_settings_file = None
fd, repo_settings_file = tempfile.mkstemp(suffix='.toml')
os.write(fd, repo_settings)
new_settings = Dynaconf(settings_files=[repo_settings_file])
for section, contents in new_settings.as_dict().items():
section_dict = copy.deepcopy(get_settings().as_dict().get(section, {}))
for key, value in contents.items():
section_dict[key] = value
get_settings().unset(section)
get_settings().set(section, section_dict, merge=False)
get_logger().info(f"Applying repo settings for section {section}, contents: {contents}")
except Exception as e:
get_logger().exception("Failed to apply repo settings", e)
finally:
if repo_settings_file:
try:
os.remove(repo_settings_file)
except Exception as e:
get_logger().error(f"Failed to remove temporary settings file {repo_settings_file}", e)

40
pr_agent/log/__init__.py Normal file
View File

@ -0,0 +1,40 @@
import json
import logging
import sys
from enum import Enum
from loguru import logger
class LoggingFormat(str, Enum):
CONSOLE = "CONSOLE"
JSON = "JSON"
def json_format(record: dict) -> str:
return record["message"]
def setup_logger(level: str = "INFO", fmt: LoggingFormat = LoggingFormat.CONSOLE):
level: int = logging.getLevelName(level.upper())
if type(level) is not int:
level = logging.INFO
if fmt == LoggingFormat.JSON:
logger.remove(None)
logger.add(
sys.stdout,
level=level,
format="{message}",
colorize=False,
serialize=True,
)
elif fmt == LoggingFormat.CONSOLE:
logger.remove(None)
logger.add(sys.stdout, level=level, colorize=True)
return logger
def get_logger(*args, **kwargs):
return logger

View File

@ -1,9 +1,8 @@
import ujson import ujson
from google.cloud import storage from google.cloud import storage
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.gitlab_provider import logger from pr_agent.log import get_logger
from pr_agent.secret_providers.secret_provider import SecretProvider from pr_agent.secret_providers.secret_provider import SecretProvider
@ -15,7 +14,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
self.bucket_name = get_settings().google_cloud_storage.bucket_name self.bucket_name = get_settings().google_cloud_storage.bucket_name
self.bucket = self.client.bucket(self.bucket_name) self.bucket = self.client.bucket(self.bucket_name)
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}") get_logger().error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}")
raise e raise e
def get_secret(self, secret_name: str) -> str: def get_secret(self, secret_name: str) -> str:
@ -23,7 +22,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
blob = self.bucket.blob(secret_name) blob = self.bucket.blob(secret_name)
return blob.download_as_string() return blob.download_as_string()
except Exception as e: except Exception as e:
logger.error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}") get_logger().error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
return "" return ""
def store_secret(self, secret_name: str, secret_value: str): def store_secret(self, secret_name: str, secret_value: str):
@ -31,5 +30,5 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
blob = self.bucket.blob(secret_name) blob = self.bucket.blob(secret_name)
blob.upload_from_string(secret_value) blob.upload_from_string(secret_value)
except Exception as e: except Exception as e:
logger.error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}") get_logger().error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}")
raise e raise e

View File

@ -1,9 +1,7 @@
import copy import copy
import hashlib import hashlib
import json import json
import logging
import os import os
import sys
import time import time
import jwt import jwt
@ -18,9 +16,10 @@ 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.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
logging.basicConfig(stream=sys.stdout, level=logging.INFO) setup_logger(fmt=LoggingFormat.JSON)
router = APIRouter() router = APIRouter()
secret_provider = get_secret_provider() secret_provider = get_secret_provider()
@ -49,7 +48,7 @@ async def get_bearer_token(shared_secret: str, client_key: str):
bearer_token = response.json()["access_token"] bearer_token = response.json()["access_token"]
return bearer_token return bearer_token
except Exception as e: except Exception as e:
logging.error(f"Failed to get bearer token: {e}") get_logger().error(f"Failed to get bearer token: {e}")
raise e raise e
@router.get("/") @router.get("/")
@ -60,21 +59,23 @@ async def handle_manifest(request: Request, response: Response):
manifest = manifest.replace("app_key", get_settings().bitbucket.app_key) manifest = manifest.replace("app_key", get_settings().bitbucket.app_key)
manifest = manifest.replace("base_url", get_settings().bitbucket.base_url) manifest = manifest.replace("base_url", get_settings().bitbucket.base_url)
except: except:
logging.error("Failed to replace api_key in Bitbucket manifest, trying to continue") get_logger().error("Failed to replace api_key in Bitbucket manifest, trying to continue")
manifest_obj = json.loads(manifest) manifest_obj = json.loads(manifest)
return JSONResponse(manifest_obj) return JSONResponse(manifest_obj)
@router.post("/webhook") @router.post("/webhook")
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request): async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
print(request.headers) log_context = {"server_type": "bitbucket_app"}
get_logger().debug(request.headers)
jwt_header = request.headers.get("authorization", None) jwt_header = request.headers.get("authorization", None)
if jwt_header: if jwt_header:
input_jwt = jwt_header.split(" ")[1] input_jwt = jwt_header.split(" ")[1]
data = await request.json() data = await request.json()
print(data) get_logger().debug(data)
async def inner(): async def inner():
try: try:
owner = data["data"]["repository"]["owner"]["username"] owner = data["data"]["repository"]["owner"]["username"]
log_context["sender"] = owner
secrets = json.loads(secret_provider.get_secret(owner)) secrets = json.loads(secret_provider.get_secret(owner))
shared_secret = secrets["shared_secret"] shared_secret = secrets["shared_secret"]
client_key = secrets["client_key"] client_key = secrets["client_key"]
@ -86,13 +87,19 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
agent = PRAgent() agent = PRAgent()
if event == "pullrequest:created": if event == "pullrequest: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["event"] = "pull_request"
with get_logger().contextualize(**log_context):
await agent.handle_request(pr_url, "review") 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["event"] = "comment"
comment_body = data["data"]["comment"]["content"]["raw"] comment_body = data["data"]["comment"]["content"]["raw"]
with get_logger().contextualize(**log_context):
await agent.handle_request(pr_url, comment_body) await agent.handle_request(pr_url, comment_body)
except Exception as e: except Exception as e:
logging.error(f"Failed to handle webhook: {e}") get_logger().error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner) background_tasks.add_task(inner)
return "OK" return "OK"
@ -103,9 +110,10 @@ async def handle_github_webhooks(request: Request, response: Response):
@router.post("/installed") @router.post("/installed")
async def handle_installed_webhooks(request: Request, response: Response): async def handle_installed_webhooks(request: Request, response: Response):
try: try:
print(request.headers) get_logger().info("handle_installed_webhooks")
get_logger().info(request.headers)
data = await request.json() data = await request.json()
print(data) get_logger().info(data)
shared_secret = data["sharedSecret"] shared_secret = data["sharedSecret"]
client_key = data["clientKey"] client_key = data["clientKey"]
username = data["principal"]["username"] username = data["principal"]["username"]
@ -115,13 +123,15 @@ async def handle_installed_webhooks(request: Request, response: Response):
} }
secret_provider.store_secret(username, json.dumps(secrets)) secret_provider.store_secret(username, json.dumps(secrets))
except Exception as e: except Exception as e:
logging.error(f"Failed to register user: {e}") get_logger().error(f"Failed to register user: {e}")
return JSONResponse({"error": "Unable to register user"}, status_code=500) return JSONResponse({"error": "Unable to register user"}, status_code=500)
@router.post("/uninstalled") @router.post("/uninstalled")
async def handle_uninstalled_webhooks(request: Request, response: Response): async def handle_uninstalled_webhooks(request: Request, response: Response):
get_logger().info("handle_uninstalled_webhooks")
data = await request.json() data = await request.json()
print(data) get_logger().info(data)
def start(): def start():

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

@ -0,0 +1,77 @@
import copy
from enum import Enum
from json import JSONDecodeError
import uvicorn
from fastapi import APIRouter, FastAPI, HTTPException
from pydantic import BaseModel
from starlette.middleware import Middleware
from starlette_context import context
from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings, global_settings
from pr_agent.log import get_logger, setup_logger
setup_logger()
router = APIRouter()
class Action(str, Enum):
review = "review"
describe = "describe"
ask = "ask"
improve = "improve"
reflect = "reflect"
answer = "answer"
class Item(BaseModel):
refspec: str
project: str
msg: str
@router.post("/api/v1/gerrit/{action}")
async def handle_gerrit_request(action: Action, item: Item):
get_logger().debug("Received a Gerrit request")
context["settings"] = copy.deepcopy(global_settings)
if action == Action.ask:
if not item.msg:
return HTTPException(
status_code=400,
detail="msg is required for ask command"
)
await PRAgent().handle_request(
f"{item.project}:{item.refspec}",
f"/{item.msg.strip()}"
)
async def get_body(request):
try:
body = await request.json()
except JSONDecodeError as e:
get_logger().error("Error parsing request body", e)
return {}
return body
@router.get("/")
async def root():
return {"status": "ok"}
def start():
# to prevent adding help messages with the output
get_settings().set("CONFIG.CLI_MODE", True)
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,23 +1,43 @@
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
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import get_logger
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 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')
GITHUB_EVENT_PATH = os.environ.get('GITHUB_EVENT_PATH') GITHUB_EVENT_PATH = os.environ.get('GITHUB_EVENT_PATH')
OPENAI_KEY = os.environ.get('OPENAI_KEY') OPENAI_KEY = os.environ.get('OPENAI_KEY') or os.environ.get('OPENAI.KEY')
OPENAI_ORG = os.environ.get('OPENAI_ORG') OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG')
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
# Check if required environment variables are set # Check if required environment variables are set
if not GITHUB_EVENT_NAME: if not GITHUB_EVENT_NAME:
print("GITHUB_EVENT_NAME not set") print("GITHUB_EVENT_NAME not set")
@ -47,13 +67,30 @@ async def run_action():
print(f"Failed to parse JSON: {e}") print(f"Failed to parse JSON: {e}")
return return
try:
get_logger().info("Applying repo settings")
pr_url = event_payload.get("pull_request", {}).get("html_url")
if pr_url:
apply_repo_settings(pr_url)
get_logger().info(f"enable_custom_labels: {get_settings().config.enable_custom_labels}")
except Exception as e:
get_logger().info(f"github action: failed to apply repo settings: {e}")
# Handle pull request event # Handle pull request event
if GITHUB_EVENT_NAME == "pull_request": if GITHUB_EVENT_NAME == "pull_request":
action = event_payload.get("action") action = event_payload.get("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 = get_setting_or_env("GITHUB_ACTION.AUTO_REVIEW", None)
if auto_review is None or is_true(auto_review):
await PRReviewer(pr_url).run() await PRReviewer(pr_url).run()
auto_describe = get_setting_or_env("GITHUB_ACTION.AUTO_DESCRIBE", None)
if is_true(auto_describe):
await PRDescription(pr_url).run()
auto_improve = get_setting_or_env("GITHUB_ACTION.AUTO_IMPROVE", None)
if is_true(auto_improve):
await PRCodeSuggestions(pr_url).run()
# Handle issue comment event # Handle issue comment event
elif GITHUB_EVENT_NAME == "issue_comment": elif GITHUB_EVENT_NAME == "issue_comment":
@ -61,12 +98,21 @@ async def run_action():
if action in ["created", "edited"]: if action in ["created", "edited"]:
comment_body = event_payload.get("comment", {}).get("body") comment_body = event_payload.get("comment", {}).get("body")
if comment_body: if comment_body:
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url") is_pr = False
if pr_url: # check if issue is pull request
if event_payload.get("issue", {}).get("pull_request"):
url = event_payload.get("issue", {}).get("pull_request", {}).get("url")
is_pr = True
else:
url = event_payload.get("issue", {}).get("url")
if url:
body = comment_body.strip().lower() body = comment_body.strip().lower()
comment_id = event_payload.get("comment", {}).get("id") comment_id = event_payload.get("comment", {}).get("id")
provider = get_git_provider()(pr_url=pr_url) provider = get_git_provider()(pr_url=url)
await PRAgent().handle_request(pr_url, body, notify=lambda: provider.add_eyes_reaction(comment_id)) if is_pr:
await PRAgent().handle_request(url, body, notify=lambda: provider.add_eyes_reaction(comment_id))
else:
await PRAgent().handle_request(url, body)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,9 +1,7 @@
import copy import copy
import logging
import sys
import os import os
import time import asyncio.locks
from typing import Any, Dict from typing import Any, Dict, List, Tuple
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
@ -15,9 +13,13 @@ from pr_agent.agent.pr_agent import PRAgent
from pr_agent.algo.utils import update_settings_from_args from pr_agent.algo.utils import update_settings_from_args
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 import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.servers.utils import verify_signature from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.git_providers.git_provider import IncrementalPR
from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.servers.utils import verify_signature, DefaultDictWithTimeout
setup_logger(fmt=LoggingFormat.JSON)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
router = APIRouter() router = APIRouter()
@ -28,11 +30,11 @@ async def handle_github_webhooks(request: Request, response: Response):
Verifies the request signature, parses the request body, and passes it to the handle_request function for further Verifies the request signature, parses the request body, and passes it to the handle_request function for further
processing. processing.
""" """
logging.debug("Received a GitHub webhook") get_logger().debug("Received a GitHub webhook")
body = await get_body(request) body = await get_body(request)
logging.debug(f'Request body:\n{body}') get_logger().debug(f'Request body:\n{body}')
installation_id = body.get("installation", {}).get("id") installation_id = body.get("installation", {}).get("id")
context["installation_id"] = installation_id context["installation_id"] = installation_id
context["settings"] = copy.deepcopy(global_settings) context["settings"] = copy.deepcopy(global_settings)
@ -44,13 +46,14 @@ async def handle_github_webhooks(request: Request, response: Response):
@router.post("/api/v1/marketplace_webhooks") @router.post("/api/v1/marketplace_webhooks")
async def handle_marketplace_webhooks(request: Request, response: Response): async def handle_marketplace_webhooks(request: Request, response: Response):
body = await get_body(request) body = await get_body(request)
logging.info(f'Request body:\n{body}') get_logger().info(f'Request body:\n{body}')
async def get_body(request): async def get_body(request):
try: try:
body = await request.json() body = await request.json()
except Exception as e: except Exception as e:
logging.error("Error parsing request body", e) get_logger().error("Error parsing request body", e)
raise HTTPException(status_code=400, detail="Error parsing request body") from e raise HTTPException(status_code=400, detail="Error parsing request body") from e
webhook_secret = getattr(get_settings().github, 'webhook_secret', None) webhook_secret = getattr(get_settings().github, 'webhook_secret', None)
if webhook_secret: if webhook_secret:
@ -60,7 +63,9 @@ async def get_body(request):
return body return body
_duplicate_requests_cache = {} _duplicate_requests_cache = DefaultDictWithTimeout(ttl=get_settings().github_app.duplicate_requests_cache_ttl)
_duplicate_push_triggers = DefaultDictWithTimeout(ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
_pending_task_duplicate_push_conditions = DefaultDictWithTimeout(asyncio.locks.Condition, ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
async def handle_request(body: Dict[str, Any], event: str): async def handle_request(body: Dict[str, Any], event: str):
@ -76,8 +81,8 @@ async def handle_request(body: Dict[str, Any], event: str):
return {} return {}
agent = PRAgent() agent = PRAgent()
bot_user = get_settings().github_app.bot_user bot_user = get_settings().github_app.bot_user
logging.info(f"action: '{action}'") sender = body.get("sender", {}).get("login")
logging.info(f"event: '{event}'") log_context = {"action": action, "event": event, "sender": sender, "server_type": "github_app"}
if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body): if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body):
return {} return {}
@ -87,54 +92,131 @@ async def handle_request(body: Dict[str, Any], event: str):
if "comment" not in body: if "comment" not in body:
return {} return {}
comment_body = body.get("comment", {}).get("body") comment_body = body.get("comment", {}).get("body")
sender = body.get("sender", {}).get("login")
if sender and bot_user in sender: if sender and bot_user in sender:
logging.info(f"Ignoring comment from {bot_user} user") get_logger().info(f"Ignoring comment from {bot_user} user")
return {} return {}
logging.info(f"Processing comment from {sender} user") get_logger().info(f"Processing comment from {sender} user")
if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]: if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
api_url = body["issue"]["pull_request"]["url"] api_url = body["issue"]["pull_request"]["url"]
elif "comment" in body and "pull_request_url" in body["comment"]: elif "comment" in body and "pull_request_url" in body["comment"]:
api_url = body["comment"]["pull_request_url"] api_url = body["comment"]["pull_request_url"]
else: else:
return {} return {}
logging.info(f"Handling comment because of event={event} and action={action}") log_context["api_url"] = api_url
get_logger().info(body)
get_logger().info(f"Handling comment because of event={event} and action={action}")
comment_id = body.get("comment", {}).get("id") comment_id = body.get("comment", {}).get("id")
provider = get_git_provider()(pr_url=api_url) provider = get_git_provider()(pr_url=api_url)
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id)) await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
# handle pull_request event: # handle pull_request event:
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft, # automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
# as well as direct review requests from the bot # as well as direct review requests from the bot
elif event == 'pull_request': elif event == 'pull_request' and action != 'synchronize':
pull_request = body.get("pull_request") pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
if not pull_request: if not (pull_request and api_url):
return {}
api_url = pull_request.get("url")
if not api_url:
return {}
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
return {} return {}
if action in get_settings().github_app.handle_pr_actions: if action in get_settings().github_app.handle_pr_actions:
if action == "review_requested": if action == "review_requested":
if body.get("requested_reviewer", {}).get("login", "") != bot_user: if body.get("requested_reviewer", {}).get("login", "") != bot_user:
return {} return {}
if pull_request.get("created_at") == pull_request.get("updated_at"): get_logger().info(f"Performing review for {api_url=} because of {event=} and {action=}")
# avoid double reviews when opening a PR for the first time await _perform_commands("pr_commands", agent, body, api_url, log_context)
# 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:
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
if not (pull_request and api_url):
return {} return {}
logging.info(f"Performing review because of event={event} and action={action}")
for command in get_settings().github_app.pr_commands: # TODO: do we still want to get the list of commits to filter bot/merge commits?
before_sha = body.get("before")
after_sha = body.get("after")
merge_commit_sha = pull_request.get("merge_commit_sha")
if before_sha == after_sha:
return {}
if get_settings().github_app.push_trigger_ignore_merge_commits and after_sha == merge_commit_sha:
return {}
if get_settings().github_app.push_trigger_ignore_bot_commits and body.get("sender", {}).get("login", "") == bot_user:
return {}
# Prevent triggering multiple times for subsequent push triggers when one is enough:
# The first push will trigger the processing, and if there's a second push in the meanwhile it will wait.
# Any more events will be discarded, because they will all trigger the exact same processing on the PR.
# We let the second event wait instead of discarding it because while the first event was being processed,
# more commits may have been pushed that led to the subsequent events,
# so we keep just one waiting as a delegate to trigger the processing for the new commits when done waiting.
current_active_tasks = _duplicate_push_triggers.setdefault(api_url, 0)
max_active_tasks = 2 if get_settings().github_app.push_trigger_pending_tasks_backlog else 1
if current_active_tasks < max_active_tasks:
# first task can enter, and second tasks too if backlog is enabled
get_logger().info(
f"Continue processing push trigger for {api_url=} because there are {current_active_tasks} active tasks"
)
_duplicate_push_triggers[api_url] += 1
else:
get_logger().info(
f"Skipping push trigger for {api_url=} because another event already triggered the same processing"
)
return {}
async with _pending_task_duplicate_push_conditions[api_url]:
if current_active_tasks == 1:
# second task waits
get_logger().info(
f"Waiting to process push trigger for {api_url=} because the first task is still in progress"
)
await _pending_task_duplicate_push_conditions[api_url].wait()
get_logger().info(f"Finished waiting to process push trigger for {api_url=} - continue with flow")
try:
if get_settings().github_app.push_trigger_wait_for_initial_review and not get_git_provider()(api_url, incremental=IncrementalPR(True)).previous_review:
get_logger().info(f"Skipping incremental review because there was no initial review for {api_url=} yet")
return {}
get_logger().info(f"Performing incremental review for {api_url=} because of {event=} and {action=}")
await _perform_commands("push_commands", agent, body, api_url, log_context)
finally:
# release the waiting task block
async with _pending_task_duplicate_push_conditions[api_url]:
_pending_task_duplicate_push_conditions[api_url].notify(1)
_duplicate_push_triggers[api_url] -= 1
get_logger().info("event or action does not require handling")
return {}
def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_user: str) -> Tuple[Dict[str, Any], str]:
invalid_result = {}, ""
pull_request = body.get("pull_request")
if not pull_request:
return invalid_result
api_url = pull_request.get("url")
if not api_url:
return invalid_result
log_context["api_url"] = api_url
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
return invalid_result
if action in ("review_requested", "synchronize") and pull_request.get("created_at") == pull_request.get("updated_at"):
# avoid double reviews when opening a PR for the first time
return invalid_result
return pull_request, api_url
async def _perform_commands(commands_conf: str, agent: PRAgent, body: dict, api_url: str, log_context: dict):
apply_repo_settings(api_url)
commands = get_settings().get(f"github_app.{commands_conf}")
for command in commands:
split_command = command.split(" ") split_command = command.split(" ")
command = split_command[0] command = split_command[0]
args = split_command[1:] args = split_command[1:]
other_args = update_settings_from_args(args) other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args) new_command = ' '.join([command] + other_args)
logging.info(f"Performing command: {new_command}") get_logger().info(body)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, new_command) await agent.handle_request(api_url, new_command)
logging.info("event or action does not require handling")
return {}
def _is_duplicate_request(body: Dict[str, Any]) -> bool: def _is_duplicate_request(body: Dict[str, Any]) -> bool:
""" """
@ -142,16 +224,11 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool:
This function checks if the request is duplicate and if so - ignores it. This function checks if the request is duplicate and if so - ignores it.
""" """
request_hash = hash(str(body)) request_hash = hash(str(body))
logging.info(f"request_hash: {request_hash}") get_logger().info(f"request_hash: {request_hash}")
request_time = time.monotonic() is_duplicate = _duplicate_requests_cache.get(request_hash, False)
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds _duplicate_requests_cache[request_hash] = True
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
for key in to_delete:
del _duplicate_requests_cache[key]
is_duplicate = request_hash in _duplicate_requests_cache
_duplicate_requests_cache[request_hash] = request_time
if is_duplicate: if is_duplicate:
logging.info(f"Ignoring duplicate request {request_hash}") get_logger().info(f"Ignoring duplicate request {request_hash}")
return is_duplicate return is_duplicate

View File

@ -1,6 +1,4 @@
import asyncio import asyncio
import logging
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
import aiohttp import aiohttp
@ -8,9 +6,10 @@ import aiohttp
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
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.servers.help import bot_help_text from pr_agent.servers.help import bot_help_text
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) setup_logger(fmt=LoggingFormat.JSON)
NOTIFICATION_URL = "https://api.github.com/notifications" NOTIFICATION_URL = "https://api.github.com/notifications"
@ -94,7 +93,7 @@ async def polling_loop():
comment_body = comment['body'] if 'body' in comment else '' comment_body = comment['body'] if 'body' in comment else ''
commenter_github_user = comment['user']['login'] \ commenter_github_user = comment['user']['login'] \
if 'user' in comment else '' if 'user' in comment else ''
logging.info(f"Commenter: {commenter_github_user}\nComment: {comment_body}") get_logger().info(f"Commenter: {commenter_github_user}\nComment: {comment_body}")
user_tag = "@" + user_id user_tag = "@" + user_id
if user_tag not in comment_body: if user_tag not in comment_body:
continue continue
@ -112,7 +111,7 @@ async def polling_loop():
print(f"Failed to fetch notifications. Status code: {response.status}") print(f"Failed to fetch notifications. Status code: {response.status}")
except Exception as e: except Exception as e:
logging.error(f"Exception during processing of a notification: {e}") get_logger().error(f"Exception during processing of a notification: {e}")
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,43 +1,84 @@
import logging import copy
import json
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI, Request, status from fastapi import APIRouter, FastAPI, Request, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from starlette.background import BackgroundTasks from starlette.background import BackgroundTasks
from starlette.middleware import Middleware
from starlette_context import context
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 from pr_agent.config_loader import get_settings, global_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider
app = FastAPI() setup_logger(fmt=LoggingFormat.JSON)
router = APIRouter() router = APIRouter()
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
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") @router.post("/webhook")
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request): async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "gitlab_app"}
if request.headers.get("X-Gitlab-Token") and secret_provider:
request_token = request.headers.get("X-Gitlab-Token")
secret = secret_provider.get_secret(request_token)
try:
secret_dict = json.loads(secret)
gitlab_token = secret_dict["gitlab_token"]
log_context["sender"] = secret_dict.get("token_name", secret_dict.get("id", "unknown"))
context["settings"] = copy.deepcopy(global_settings)
context["settings"].gitlab.personal_access_token = gitlab_token
except Exception as e:
get_logger().error(f"Failed to validate secret {request_token}: {e}")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
elif get_settings().get("GITLAB.SHARED_SECRET"):
secret = get_settings().get("GITLAB.SHARED_SECRET")
if not request.headers.get("X-Gitlab-Token") == secret:
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
else:
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_token:
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
data = await request.json() data = await request.json()
get_logger().info(json.dumps(data))
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']: if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
logging.info(f"A merge request has been opened: {data['object_attributes'].get('title')}") get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
url = data['object_attributes'].get('url') url = data['object_attributes'].get('url')
background_tasks.add_task(PRAgent().handle_request, url, "/review") handle_request(background_tasks, url, "/review", log_context)
elif data.get('object_kind') == 'note' and data['event_type'] == 'note': elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
if 'merge_request' in data: if 'merge_request' in data:
mr = data['merge_request'] mr = data['merge_request']
url = mr.get('url') url = mr.get('url')
body = data.get('object_attributes', {}).get('note') body = data.get('object_attributes', {}).get('note')
background_tasks.add_task(PRAgent().handle_request, url, body) handle_request(background_tasks, url, body, log_context)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"})) return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
@router.get("/")
async def root():
return {"status": "ok"}
def start(): def start():
gitlab_url = get_settings().get("GITLAB.URL", None) gitlab_url = get_settings().get("GITLAB.URL", None)
if not gitlab_url: if not gitlab_url:
raise ValueError("GITLAB.URL is not set") raise ValueError("GITLAB.URL is not set")
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_token:
raise ValueError("GITLAB.PERSONAL_ACCESS_TOKEN is not set")
get_settings().config.git_provider = "gitlab" get_settings().config.git_provider = "gitlab"
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI() app = FastAPI(middleware=middleware)
app.include_router(router) app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=3000) uvicorn.run(app, host="0.0.0.0", port=3000)

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" \
"> see the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n\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" \ ">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \
">To list the possible configuration parameters, use the **/config** command.\n" \ ">To list the possible configuration parameters, 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

@ -1,14 +1,13 @@
import logging
from fastapi import FastAPI from fastapi import FastAPI
from mangum import Mangum from mangum import Mangum
from starlette.middleware import Middleware
from starlette_context.middleware import RawContextMiddleware
from pr_agent.servers.github_app import router from pr_agent.servers.github_app import router
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
app = FastAPI() middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router) app.include_router(router)
handler = Mangum(app, lifespan="off") handler = Mangum(app, lifespan="off")

View File

@ -1,5 +1,8 @@
import hashlib import hashlib
import hmac import hmac
import time
from collections import defaultdict
from typing import Callable, Any
from fastapi import HTTPException from fastapi import HTTPException
@ -25,3 +28,59 @@ def verify_signature(payload_body, secret_token, signature_header):
class RateLimitExceeded(Exception): class RateLimitExceeded(Exception):
"""Raised when the git provider API rate limit has been exceeded.""" """Raised when the git provider API rate limit has been exceeded."""
pass pass
class DefaultDictWithTimeout(defaultdict):
"""A defaultdict with a time-to-live (TTL)."""
def __init__(
self,
default_factory: Callable[[], Any] = None,
ttl: int = None,
refresh_interval: int = 60,
update_key_time_on_get: bool = True,
*args,
**kwargs,
):
"""
Args:
default_factory: The default factory to use for keys that are not in the dictionary.
ttl: The time-to-live (TTL) in seconds.
refresh_interval: How often to refresh the dict and delete items older than the TTL.
update_key_time_on_get: Whether to update the access time of a key also on get (or only when set).
"""
super().__init__(default_factory, *args, **kwargs)
self.__key_times = dict()
self.__ttl = ttl
self.__refresh_interval = refresh_interval
self.__update_key_time_on_get = update_key_time_on_get
self.__last_refresh = self.__time() - self.__refresh_interval
@staticmethod
def __time():
return time.monotonic()
def __refresh(self):
if self.__ttl is None:
return
request_time = self.__time()
if request_time - self.__last_refresh > self.__refresh_interval:
return
to_delete = [key for key, key_time in self.__key_times.items() if request_time - key_time > self.__ttl]
for key in to_delete:
del self[key]
self.__last_refresh = request_time
def __getitem__(self, __key):
if self.__update_key_time_on_get:
self.__key_times[__key] = self.__time()
self.__refresh()
return super().__getitem__(__key)
def __setitem__(self, __key, __value):
self.__key_times[__key] = self.__time()
return super().__setitem__(__key, __value)
def __delitem__(self, __key):
del self.__key_times[__key]
return super().__delitem__(__key)

View File

@ -16,6 +16,10 @@ key = "" # Acquire through https://platform.openai.com
#deployment_id = "" # The deployment name you chose when you deployed the engine #deployment_id = "" # The deployment name you chose when you deployed the engine
#fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id #fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id
[pinecone]
api_key = "..."
environment = "gcp-starter"
[anthropic] [anthropic]
key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/ key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/
@ -24,6 +28,21 @@ key = "" # Optional, uncomment if you want to use Cohere. Acquire through https:
[replicate] [replicate]
key = "" # Optional, uncomment if you want to use Replicate. Acquire through https://replicate.com/ key = "" # Optional, uncomment if you want to use Replicate. Acquire through https://replicate.com/
[huggingface]
key = "" # Optional, uncomment if you want to use Huggingface Inference API. Acquire through https://huggingface.co/docs/api-inference/quicktour
api_base = "" # the base url for your huggingface inference endpoint
[ollama]
api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/
[vertexai]
vertex_project = "" # the google cloud platform project name 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.
@ -43,5 +62,12 @@ webhook_secret = "<WEBHOOK SECRET>" # Optional, may be commented out.
personal_access_token = "" personal_access_token = ""
[bitbucket] [bitbucket]
# Bitbucket personal bearer token # For Bitbucket personal/repository bearer token
bearer_token = "" bearer_token = ""
# For Bitbucket app
app_key = ""
base_url = ""
[litellm]
LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token

View File

@ -1,5 +1,5 @@
[config] [config]
model="gpt-4" model="gpt-4" # "gpt-4-1106-preview"
fallback_models=["gpt-3.5-turbo-16k"] fallback_models=["gpt-3.5-turbo-16k"]
git_provider="github" git_provider="github"
publish_output=true publish_output=true
@ -10,30 +10,58 @@ use_repo_settings_file=true
ai_timeout=180 ai_timeout=180
max_description_tokens = 500 max_description_tokens = 500
max_commits_tokens = 500 max_commits_tokens = 500
litellm_debugger=false max_model_tokens = 32000 # Limits the maximum number of tokens that can be used by any model, regardless of the model's default capabilities.
patch_extra_lines = 3
secret_provider="google_cloud_storage" secret_provider="google_cloud_storage"
cli_mode=false
[pr_reviewer] # /review # [pr_reviewer] # /review #
# enable/disable features
require_focused_review=false require_focused_review=false
require_score_review=false require_score_review=false
require_tests_review=true require_tests_review=true
require_security_review=true require_security_review=true
require_estimate_effort_to_review=true
# general options
num_code_suggestions=4 num_code_suggestions=4
inline_code_comments = false inline_code_comments = false
ask_and_reflect=false ask_and_reflect=false
automatic_review=true automatic_review=true
remove_previous_review_comment=false
persistent_comment=true
extra_instructions = "" extra_instructions = ""
# review labels
enable_review_labels_security=true
enable_review_labels_effort=false
# specific configurations for incremental review (/review -i)
require_all_thresholds_for_incremental_review=false
minimal_commits_for_incremental_review=0
minimal_minutes_for_incremental_review=0
[pr_description] # /describe # [pr_description] # /describe #
publish_labels=true
publish_description_as_comment=false publish_description_as_comment=false
add_original_user_description=false add_original_user_description=false
keep_original_user_title=false keep_original_user_title=false
use_bullet_points=true
extra_instructions = "" extra_instructions = ""
enable_pr_type=true
enable_file_walkthrough=false
enable_semantic_files_types=true
final_update_message = true
# markers
use_description_markers=false
include_generated_by_header=true
#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
@ -42,6 +70,10 @@ rank_extended_suggestions = true
max_number_of_calls = 5 max_number_of_calls = 5
final_clip_factor = 0.9 final_clip_factor = 0.9
[pr_add_docs] # /add_docs #
extra_instructions = ""
docs_style = "Sphinx Style" # "Google Style with Args, Returns, Attributes...etc", "Numpy Style", "Sphinx Style", "PEP257", "reStructuredText"
[pr_update_changelog] # /update_changelog # [pr_update_changelog] # /update_changelog #
push_changelog_changes=false push_changelog_changes=false
extra_instructions = "" extra_instructions = ""
@ -52,6 +84,12 @@ 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]
# auto_review = true # set as env var in .github/workflows/pr-agent.yaml
# auto_describe = true # set as env var in .github/workflows/pr-agent.yaml
# auto_improve = true # set as env var in .github/workflows/pr-agent.yaml
[github_app] [github_app]
# these toggles allows running the github app from custom deployments # these toggles allows running the github app from custom deployments
@ -67,6 +105,30 @@ pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true", "/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/auto_review", "/auto_review",
] ]
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
handle_push_trigger = false
push_trigger_ignore_bot_commits = true
push_trigger_ignore_merge_commits = true
push_trigger_wait_for_initial_review = true
push_trigger_pending_tasks_backlog = true
push_trigger_pending_tasks_ttl = 300
push_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"""/auto_review -i \
--pr_reviewer.require_focused_review=false \
--pr_reviewer.require_score_review=false \
--pr_reviewer.require_tests_review=false \
--pr_reviewer.require_security_review=false \
--pr_reviewer.require_estimate_effort_to_review=false \
--pr_reviewer.num_code_suggestions=0 \
--pr_reviewer.inline_code_comments=false \
--pr_reviewer.remove_previous_review_comment=true \
--pr_reviewer.require_all_thresholds_for_incremental_review=false \
--pr_reviewer.minimal_commits_for_incremental_review=5 \
--pr_reviewer.minimal_minutes_for_incremental_review=30 \
--pr_reviewer.extra_instructions='' \
"""
]
[gitlab] [gitlab]
# URL to the gitlab service # URL to the gitlab service
@ -85,3 +147,26 @@ polling_interval_seconds = 30
# 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"
# review_path= "path/to/review.md" # review_path= "path/to/review.md"
[gerrit]
# endpoint to the gerrit service
# url = "ssh://gerrit.example.com:29418"
# user for gerrit authentication
# user = "ai-reviewer"
# patch server where patches will be saved
# patch_server_endpoint = "http://127.0.0.1:5000/patch"
# token to authenticate in the patch server
# patch_server_token = ""
[litellm]
#use_client = false
[pr_similar_issue]
skip_comments = false
force_update_dataset = false
max_issues_to_scan = 500
[pinecone]
# fill and place in .secrets.toml
#api_key = ...
# environment = "gcp-starter"

View File

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

View File

@ -0,0 +1,11 @@
[ignore]
glob = [
# Ignore files and directories matching these glob patterns.
# See https://docs.python.org/3/library/glob.html
'vendor/**',
]
regex = [
# Ignore files and directories matching these regex patterns.
# See https://learnbyexample.github.io/python-regex-cheatsheet/
]

View File

@ -53,7 +53,8 @@ default = [
'xz', 'xz',
'zip', 'zip',
'zst', 'zst',
'snap' 'snap',
'lockb'
] ]
extra = [ extra = [
'md', 'md',
@ -432,3 +433,6 @@ reStructuredText = [".rst", ".rest", ".rest.txt", ".rst.txt", ]
wisp = [".wisp", ] wisp = [".wisp", ]
xBase = [".prg", ".prw", ] xBase = [".prg", ".prw", ]
[docs_blacklist_extensions]
# Disable docs for these extensions of text files and scripts that are not programming languages of function, classes and methods
docs_blacklist = ['sql', 'txt', 'yaml', 'json', 'xml', 'md', 'rst', 'rest', 'rest.txt', 'rst.txt', 'mdpolicy', 'mdown', 'markdown', 'mdwn', 'mkd', 'mkdn', 'mkdown', 'sh']

View File

@ -0,0 +1,126 @@
[pr_add_docs_prompt]
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 {{ docs_for_language }} for code components in the PR Diff.
Example for the PR Diff format:
======
## src/file1.py
@@ -12,3 +12,4 @@ def func1():
__new hunk__
12 code line1 that remained unchanged in the PR
14 +new code line1 added in the PR
15 +new code line2 added in the PR
16 code line2 that remained unchanged in the PR
__old hunk__
code line1 that remained unchanged in the PR
-code line that was removed in the PR
code line2 that remained unchanged in the PR
@@ ... @@ def func2():
__new hunk__
...
__old hunk__
...
## src/file2.py
...
======
Specific instructions:
- 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.
- 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.
- The {{ docs_for_language }} should be in standard format.
- Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added.
{%- if extra_instructions %}
Extra instructions from the user:
======
{{ extra_instructions }}
======
{%- endif %}
You must use the following YAML schema to format your answer:
```yaml
Code Documentation:
type: array
uniqueItems: true
items:
relevant file:
type: string
description: the relevant file full path
relevant line:
type: integer
description: |-
The relevant line number from a '__new hunk__' section where the {{ docs_for_language }} should be added.
doc placement:
type: string
enum:
- before
- after
description: |-
The {{ docs_for_language }} placement relative to the relevant line (code component).
documentation:
type: string
description: |-
The {{ docs_for_language }} content. It should be complete, correctly formatted and indented, and without line numbers.
```
Example output:
```yaml
Code Documentation:
- relevant file: |-
src/file1.py
relevant lines: 12
doc placement: after
documentation: |-
\"\"\"
This is a python docstring for func1.
\"\"\"
- ...
...
```
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-').
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
"""
user="""PR Info:
Title: '{{ title }}'
Branch: '{{ branch }}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %}
Main PR language: '{{language}}'
{%- endif %}
The PR Diff:
======
{{ diff|trim }}
======
Response (should be a valid YAML, and nothing else):
```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. 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,27 +27,29 @@ __old hunk__
## src/file2.py ## src/file2.py
... ...
' ======
Specific instructions: Specific instructions:
- Provide up to {{ num_code_suggestions }} code 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 or type hints.
Try to provide diverse and insightful suggestions.
- 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:
@ -68,12 +69,17 @@ Code suggestions:
type: string type: string
description: |- description: |-
a code snippet showing the relevant code lines from a '__new hunk__' section. a code snippet showing the relevant code lines from a '__new hunk__' section.
It must be continuous, correctly formatted and indented, and without line numbers. It must be contiguous, correctly formatted and indented, and without line numbers.
relevant lines: relevant lines start:
type: string type: integer
description: |- description: |-
the relevant lines from a '__new hunk__' section, in the format of 'start_line-end_line'. The relevant line number from a '__new hunk__' section where the suggestion starts (inclusive).
For example: '10-15'. They should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above. Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
relevant lines end:
type: integer
description: |-
The relevant line number from a '__new hunk__' section where the suggestion ends (inclusive).
Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
improved code: improved code:
type: string type: string
description: |- description: |-
@ -84,15 +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: '12-12' relevant lines start: |-
12
relevant lines end: |-
12
improved code: |- improved code: |-
... ...
...
``` ```
@ -106,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

@ -0,0 +1,86 @@
[pr_custom_labels_prompt]
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to provide labels that describe the PR content.
{%- if enable_custom_labels %}
Thoroughly read the labels name and the provided description, and decide whether the label is relevant to the PR.
{%- endif %}
{%- if extra_instructions %}
Extra instructions from the user:
======
{{ extra_instructions }}
======
{% endif %}
The output must be a YAML object equivalent to type $Labels, according to the following Pydantic definitions:
======
{%- if enable_custom_labels %}
{{ custom_labels_class }}
{%- else %}
class Label(str, Enum):
bug_fix = "Bug fix"
tests = "Tests"
enhancement = "Enhancement"
documentation = "Documentation"
other = "Other"
{%- 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:
```yaml
labels:
- ...
- ...
```
Answer should be a valid YAML, and nothing else.
"""
user="""PR Info:
Previous title: '{{title}}'
Branch: '{{ branch }}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %}
Main PR language: '{{ language }}'
{%- endif %}
{%- if commit_messages_str %}
Commit messages:
======
{{ commit_messages_str|trim }}
======
{%- endif %}
The PR Git 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.
Response (should be a valid YAML, and nothing else):
```yaml
"""

View File

@ -1,86 +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 the PR content. Your task is to provide a full description for the PR content - title, type, description, and main files walkthrough.
- Make sure not to focus 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.
- If needed, each YAML output should be in block scalar format ('|-') - The generated title and description should prioritize the most significant changes.
- 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: array enhancement = "Enhancement"
items: documentation = "Documentation"
type: string other = "Other"
enum:
- Bug fix {%- if enable_custom_labels %}
- Tests
- Bug fix with tests {{ custom_labels_class }}
- Refactoring
- Enhancement {%- endif %}
- Documentation
- Other {%- if enable_file_walkthrough %}
PR Description: class FileWalkthrough(BaseModel):
type: string filename: str = Field(description="the relevant file full path")
description: an informative and concise description of the PR changes_in_file: str = Field(description="minimal and concise summary of the changes in the relevant file")
PR Main Files Walkthrough: {%- endif %}
type: array
maxItems: 10 {%- if enable_semantic_files_types %}
description: |- Class FileDescription(BaseModel):
a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files). filename: str = Field(description="the relevant file full path")
items: changes_summary: str = Field(description="minimal and concise summary of the changes in the relevant file")
filename: 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', ...")
type: string {%- endif %}
description: the relevant file full path
changes in file: Class PRDescription(BaseModel):
type: string title: str = Field(description="an informative title for the PR, describing its main theme")
description: minimal and concise description of the changes in the relevant file 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:
- Bug fix - ...
PR Description: |- - ...
{%- if enable_custom_labels %}
labels:
- ...
- ...
{%- endif %}
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,24 +21,28 @@ code line that already existed in the file....
## src/file2.py ## src/file2.py
... ...
' ======
Thre 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 %}
- Provide up to {{ num_code_suggestions }} code suggestions.
Code suggestions guidelines:
- 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 or type hints. - 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:
@ -52,7 +57,6 @@ PR Analysis:
enum: enum:
- Bug fix - Bug fix
- Tests - Tests
- Refactoring
- Enhancement - Enhancement
- Documentation - Documentation
- Other - Other
@ -85,14 +89,22 @@ PR Analysis:
code diff changes are too scattered, then the PR is not focused. Explain code diff changes are too scattered, then the PR is not focused. Explain
your answer shortly. your answer shortly.
{%- endif %} {%- endif %}
{%- if require_estimate_effort_to_review %}
Estimated effort to review [1-5]:
type: string
description: >-
Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review.
Take into account the size, complexity, quality, and the needed changes of the PR code diff.
Explain your answer shortly (1-2 sentences). Use the format: '1, because ...'
{%- endif %}
PR Feedback: PR Feedback:
General suggestions: General suggestions:
type: string type: string
description: |- description: |-
General suggestions and feedback for the contributors and maintainers of General suggestions and feedback for the contributors and maintainers of this PR.
this PR. May include important suggestions for the overall structure, May include important suggestions for the overall structure,
primary purpose, best practices, critical bugs, and other aspects of the primary purpose, best practices, critical bugs, and other aspects of the PR.
PR. Don't address PR title and description, or lack of tests. Explain your suggestions. Don't address PR title and description, or lack of tests. Explain your suggestions.
{%- if num_code_suggestions > 0 %} {%- if num_code_suggestions > 0 %}
Code feedback: Code feedback:
type: array type: array
@ -105,11 +117,10 @@ PR Feedback:
suggestion: suggestion:
type: string type: string
description: |- description: |-
a concrete suggestion for meaningfully improving the new PR code. Also a concrete suggestion for meaningfully improving the new PR code.
describe how, specifically, the suggestion can be applied to new PR Also describe how, specifically, the suggestion can be applied to new PR code.
code. Add tags with importance measure that matches each suggestion Add tags with importance measure that matches each suggestion ('important' or 'medium').
('important' or 'medium'). Do not make suggestions for updating or Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.
adding docstrings, renaming PR title and description, or linter like.
relevant line: relevant line:
type: string type: string
description: |- description: |-
@ -121,8 +132,8 @@ PR Feedback:
Security concerns: Security concerns:
type: string type: string
description: >- description: >-
yes\\no question: does this PR code introduce possible security concerns or does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' if there are no possible issues.
issues, like SQL injection, XSS, CSRF, and others ? If answered 'yes',explain your answer shortly Answer 'Yes, because ...' if there are security concerns or issues. Explain your answer shortly.
{%- endif %} {%- endif %}
``` ```
@ -134,7 +145,7 @@ PR Analysis:
PR summary: |- PR summary: |-
xxx xxx
Type of PR: |- Type of PR: |-
Bug fix ...
{%- if require_score %} {%- if require_score %}
Score: 89 Score: 89
{%- endif %} {%- endif %}
@ -143,6 +154,10 @@ PR Analysis:
{%- if require_focused %} {%- if require_focused %}
Focused PR: no, because ... Focused PR: no, because ...
{%- endif %} {%- endif %}
{%- if require_estimate_effort_to_review %}
Estimated effort to review [1-5]: |-
3, because ...
{%- endif %}
PR Feedback: PR Feedback:
General PR suggestions: |- General PR suggestions: |-
... ...
@ -166,34 +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,28 +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:
``` ```
@ -37,9 +53,10 @@ Current date:
``` ```
The current CHANGELOG.md: The current CHANGELOG.md:
``` ======
{{ changelog_file_str }} {{ changelog_file_str }}
``` ======
Response: Response:
""" """

View File

@ -0,0 +1,179 @@
import copy
import textwrap
from typing import Dict
from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRAddDocs:
def __init__(self, pr_url: str, cli_mode=False, args: list = None):
self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self.ai_handler = AiHandler()
self.patches_diff = None
self.prediction = None
self.cli_mode = cli_mode
self.vars = {
"title": self.git_provider.pr.title,
"branch": self.git_provider.get_pr_branch(),
"description": self.git_provider.get_pr_description(),
"language": self.main_language,
"diff": "", # empty diff for initial calculation
"extra_instructions": get_settings().pr_add_docs.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(),
'docs_for_language': get_docs_for_language(self.main_language,
get_settings().pr_add_docs.docs_style),
}
self.token_handler = TokenHandler(self.git_provider.pr,
self.vars,
get_settings().pr_add_docs_prompt.system,
get_settings().pr_add_docs_prompt.user)
async def run(self):
try:
get_logger().info('Generating code Docs for PR...')
if get_settings().config.publish_output:
self.git_provider.publish_comment("Generating Documentation...", is_temporary=True)
get_logger().info('Preparing PR documentation...')
await retry_with_fallback_models(self._prepare_prediction)
data = self._prepare_pr_code_docs()
if (not data) or (not 'Code Documentation' in data):
get_logger().info('No code documentation found for PR.')
return
if get_settings().config.publish_output:
get_logger().info('Pushing PR documentation...')
self.git_provider.remove_initial_comment()
get_logger().info('Pushing inline code documentation...')
self.push_inline_docs(data)
except Exception as e:
get_logger().error(f"Failed to generate code documentation for PR, error: {e}")
async def _prepare_prediction(self, model: str):
get_logger().info('Getting PR diff...')
# Disable adding docs to scripts and other non-relevant text files
from pr_agent.algo.language_handler import bad_extensions
bad_extensions += get_settings().docs_blacklist_extensions.docs_blacklist
self.patches_diff = get_pr_diff(self.git_provider,
self.token_handler,
model,
add_line_numbers_to_hunks=True,
disable_extra_lines=False)
get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str):
variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined)
system_prompt = environment.from_string(get_settings().pr_add_docs_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_add_docs_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt)
return response
def _prepare_pr_code_docs(self) -> Dict:
docs = self.prediction.strip()
data = load_yaml(docs)
if isinstance(data, list):
data = {'Code Documentation': data}
return data
def push_inline_docs(self, data):
docs = []
if not data['Code Documentation']:
return self.git_provider.publish_comment('No code documentation found to improve this PR.')
for d in data['Code Documentation']:
try:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"add_docs: {d}")
relevant_file = d['relevant file'].strip()
relevant_line = int(d['relevant line']) # absolute position
documentation = d['documentation']
doc_placement = d['doc placement'].strip()
if documentation:
new_code_snippet = self.dedent_code(relevant_file, relevant_line, documentation, doc_placement,
add_original_line=True)
body = f"**Suggestion:** Proposed documentation\n```suggestion\n" + new_code_snippet + "\n```"
docs.append({'body': body, 'relevant_file': relevant_file,
'relevant_lines_start': relevant_line,
'relevant_lines_end': relevant_line})
except Exception:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not parse code docs: {d}")
is_successful = self.git_provider.publish_code_suggestions(docs)
if not is_successful:
get_logger().info("Failed to publish code docs, trying to publish each docs separately")
for doc_suggestion in docs:
self.git_provider.publish_code_suggestions([doc_suggestion])
def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet, doc_placement='after',
add_original_line=False):
try: # dedent code snippet
self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files \
else self.git_provider.get_diff_files()
original_initial_line = None
for file in self.diff_files:
if file.filename.strip() == relevant_file:
original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1]
break
if original_initial_line:
if doc_placement == 'after':
line = file.head_file.splitlines()[relevant_lines_start]
else:
line = original_initial_line
suggested_initial_line = new_code_snippet.splitlines()[0]
original_initial_spaces = len(line) - len(line.lstrip())
suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip())
delta_spaces = original_initial_spaces - suggested_initial_spaces
if delta_spaces > 0:
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
if add_original_line:
if doc_placement == 'after':
new_code_snippet = original_initial_line + "\n" + new_code_snippet
else:
new_code_snippet = new_code_snippet.rstrip() + "\n" + original_initial_line
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
return new_code_snippet
def get_docs_for_language(language, style):
language = language.lower()
if language == 'java':
return "Javadocs"
elif language in ['python', 'lisp', 'clojure']:
return f"Docstring ({style})"
elif language in ['javascript', 'typescript']:
return "JSdocs"
elif language == 'c++':
return "Doxygen"
else:
return "Docs"

View File

@ -1,16 +1,16 @@
import copy import copy
import logging
import textwrap import textwrap
from typing import List, Dict 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
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, get_pr_multi_diffs from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml from pr_agent.algo.utils import load_yaml
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import BitbucketProvider, get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRCodeSuggestions: class PRCodeSuggestions:
@ -22,7 +22,10 @@ class PRCodeSuggestions:
) )
# extended mode # extended mode
try:
self.is_extended = any(["extended" in arg for arg in args]) self.is_extended = any(["extended" in arg for arg in args])
except:
self.is_extended = False
if self.is_extended: if self.is_extended:
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk
else: else:
@ -48,37 +51,47 @@ class PRCodeSuggestions:
get_settings().pr_code_suggestions_prompt.user) get_settings().pr_code_suggestions_prompt.user)
async def run(self): async def run(self):
logging.info('Generating code suggestions for PR...') try:
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)
logging.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()
else: else:
data = await retry_with_fallback_models(self._prepare_prediction_extended) data = await retry_with_fallback_models(self._prepare_prediction_extended)
if (not data) or (not 'Code suggestions' in data):
get_logger().info('No code suggestions found for PR.')
return
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \ if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
(self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions): (self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions):
logging.info('Ranking Suggestions...') get_logger().info('Ranking Suggestions...')
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:
logging.info('Pushing PR review...') get_logger().info('Pushing PR code suggestions...')
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
logging.info('Pushing inline code suggestions...') if get_settings().pr_code_suggestions.summarize:
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) self.push_inline_code_suggestions(data)
except Exception as e:
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
async def _prepare_prediction(self, model: str): async def _prepare_prediction(self, model: str):
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.patches_diff = get_pr_diff(self.git_provider,
self.token_handler, self.token_handler,
model, model,
add_line_numbers_to_hunks=True, add_line_numbers_to_hunks=True,
disable_extra_lines=True) disable_extra_lines=True)
logging.info('Getting AI prediction...') get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str): async def _get_prediction(self, model: str):
@ -88,8 +101,8 @@ class PRCodeSuggestions:
system_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt) system=system_prompt, user=user_prompt)
@ -106,18 +119,16 @@ 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']:
try: try:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"suggestion: {d}") get_logger().info(f"suggestion: {d}")
relevant_file = d['relevant file'].strip() relevant_file = d['relevant file'].strip()
relevant_lines_str = d['relevant lines'].strip() relevant_lines_start = int(d['relevant lines start']) # absolute position
if ',' in relevant_lines_str: # handling 'relevant lines': '181, 190' or '178-184, 188-194' relevant_lines_end = int(d['relevant lines end'])
relevant_lines_str = relevant_lines_str.split(',')[0]
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
relevant_lines_end = int(relevant_lines_str.split('-')[-1])
content = d['suggestion content'] content = d['suggestion content']
new_code_snippet = d['improved code'] new_code_snippet = d['improved code']
@ -130,11 +141,11 @@ class PRCodeSuggestions:
'relevant_lines_end': relevant_lines_end}) 'relevant_lines_end': relevant_lines_end})
except Exception: except Exception:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Could not parse suggestion: {d}") get_logger().info(f"Could not parse suggestion: {d}")
is_successful = self.git_provider.publish_code_suggestions(code_suggestions) is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
if not is_successful: if not is_successful:
logging.info("Failed to publish code suggestions, trying to publish each suggestion separately") get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately")
for code_suggestion in code_suggestions: for code_suggestion in code_suggestions:
self.git_provider.publish_code_suggestions([code_suggestion]) self.git_provider.publish_code_suggestions([code_suggestion])
@ -156,19 +167,19 @@ class PRCodeSuggestions:
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n') new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") get_logger().info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
return new_code_snippet return new_code_snippet
async def _prepare_prediction_extended(self, model: str) -> dict: async def _prepare_prediction_extended(self, model: str) -> dict:
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model, patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
max_calls=get_settings().pr_code_suggestions.max_number_of_calls) max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
logging.info('Getting multi AI predictions...') get_logger().info('Getting multi AI predictions...')
prediction_list = [] prediction_list = []
for i, patches_diff in enumerate(patches_diff_list): for i, patches_diff in enumerate(patches_diff_list):
logging.info(f"Processing chunk {i + 1} of {len(patches_diff_list)}") get_logger().info(f"Processing chunk {i + 1} of {len(patches_diff_list)}")
self.patches_diff = patches_diff self.patches_diff = patches_diff
prediction = await self._get_prediction(model) prediction = await self._get_prediction(model)
prediction_list.append(prediction) prediction_list.append(prediction)
@ -216,8 +227,8 @@ class PRCodeSuggestions:
variables) variables)
user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt, response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt,
user=user_prompt) user=user_prompt)
@ -232,9 +243,46 @@ class PRCodeSuggestions:
data_sorted = data_sorted[:new_len] data_sorted = data_sorted[:new_len]
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 1: if get_settings().config.verbosity_level >= 1:
logging.info(f"Could not sort suggestions, error: {e}") get_logger().info(f"Could not sort suggestions, error: {e}")
data_sorted = suggestion_list data_sorted = suggestion_list
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

@ -1,7 +1,6 @@
import logging
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.log import get_logger
class PRConfig: class PRConfig:
@ -19,11 +18,11 @@ class PRConfig:
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
async def run(self): async def run(self):
logging.info('Getting configuration settings...') get_logger().info('Getting configuration settings...')
logging.info('Preparing configs...') get_logger().info('Preparing configs...')
pr_comment = self._prepare_pr_configs() pr_comment = self._prepare_pr_configs()
if get_settings().config.publish_output: if get_settings().config.publish_output:
logging.info('Pushing configs...') get_logger().info('Pushing configs...')
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
return "" return ""
@ -44,5 +43,5 @@ class PRConfig:
comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}" comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
comment_str += " " comment_str += " "
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"comment_str:\n{comment_str}") get_logger().info(f"comment_str:\n{comment_str}")
return comment_str return comment_str

View File

@ -1,6 +1,5 @@
import copy import copy
import json import re
import logging
from typing import List, Tuple from typing import List, Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
@ -8,10 +7,11 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRDescription: class PRDescription:
@ -28,6 +28,12 @@ class PRDescription:
self.main_pr_language = get_main_pr_language( self.main_pr_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files() self.git_provider.get_languages(), self.git_provider.get_files()
) )
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()
@ -36,11 +42,16 @@ class PRDescription:
self.vars = { self.vars = {
"title": self.git_provider.pr.title, "title": self.git_provider.pr.title,
"branch": self.git_provider.get_pr_branch(), "branch": self.git_provider.get_pr_branch(),
"description": self.git_provider.get_pr_description(), "description": self.git_provider.get_pr_description(full=False),
"language": self.main_pr_language, "language": self.main_pr_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
"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(),
"enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
"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()
@ -61,27 +72,53 @@ class PRDescription:
""" """
Generates a PR description using an AI model and publishes it to the PR. Generates a PR description using an AI model and publishes it to the PR.
""" """
logging.info('Generating a PR description...')
try:
get_logger().info(f"Generating a PR description {self.pr_id}")
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing pr description...", is_temporary=True) self.git_provider.publish_comment("Preparing PR description...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
logging.info('Preparing answer...') get_logger().info(f"Preparing answer {self.pr_id}")
pr_title, pr_body, pr_types, markdown_text = self._prepare_pr_answer() if self.prediction:
self._prepare_data()
else:
return None
if get_settings().pr_description.enable_semantic_files_types:
self._prepare_file_labels()
pr_labels = []
if get_settings().pr_description.publish_labels:
pr_labels = self._prepare_labels()
if get_settings().pr_description.use_description_markers:
pr_title, pr_body = self._prepare_pr_answer_with_markers()
else:
pr_title, pr_body, = self._prepare_pr_answer()
full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}"
if get_settings().config.publish_output: if get_settings().config.publish_output:
logging.info('Pushing answer...') 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:
self.git_provider.publish_comment(markdown_text) 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 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_labels()
if current_labels is None: user_labels = get_user_labels(current_labels)
current_labels = [] self.git_provider.publish_labels(pr_labels + user_labels)
self.git_provider.publish_labels(pr_types + current_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:
get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
return "" return ""
@ -99,9 +136,12 @@ class PRDescription:
Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions. Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions.
""" """
logging.info('Getting PR diff...') if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
return None
get_logger().info(f"Getting PR diff {self.pr_id}")
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
logging.info('Getting AI prediction...') get_logger().info(f"Getting AI prediction {self.pr_id}")
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str) -> str: async def _get_prediction(self, model: str) -> str:
@ -118,12 +158,13 @@ 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)
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)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion( response, finish_reason = await self.ai_handler.chat_completion(
model=model, model=model,
@ -132,36 +173,89 @@ class PRDescription:
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_pr_answer(self) -> Tuple[str, str, List[str], str]: def _prepare_data(self):
# Load the AI prediction data into a dictionary
self.data = load_yaml(self.prediction.strip())
if get_settings().pr_description.add_original_user_description and self.user_description:
self.data["User Description"] = self.user_description
def _prepare_labels(self) -> List[str]:
pr_types = []
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'labels' in self.data:
if type(self.data['labels']) == list:
pr_types = self.data['labels']
elif type(self.data['labels']) == str:
pr_types = self.data['labels'].split(',')
elif 'type' in self.data:
if type(self.data['type']) == list:
pr_types = self.data['type']
elif type(self.data['type']) == str:
pr_types = self.data['type'].split(',')
return pr_types
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]:
get_logger().info(f"Using description marker replacements {self.pr_id}")
title = self.vars["title"]
body = self.user_description
if get_settings().pr_description.include_generated_by_header:
ai_header = f"### 🤖 Generated by PR Agent at {self.git_provider.last_commit_id.sha}\n\n"
else:
ai_header = ""
ai_type = self.data.get('type')
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
pr_type = f"{ai_header}{ai_type}"
body = body.replace('pr_agent:type', pr_type)
ai_summary = self.data.get('description')
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
summary = f"{ai_header}{ai_summary}"
body = body.replace('pr_agent:summary', summary)
if not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
ai_walkthrough = self.data.get('PR Main Files Walkthrough')
if ai_walkthrough:
walkthrough = str(ai_header)
for file in ai_walkthrough:
filename = file['filename'].replace("'", "`")
description = file['changes in file'].replace("'", "`")
walkthrough += f'- `{filename}`: {description}\n'
body = body.replace('pr_agent:walkthrough', walkthrough)
return title, body
def _prepare_pr_answer(self) -> Tuple[str, str]:
""" """
Prepare the PR description based on the AI prediction data. Prepare the PR description based on the AI prediction data.
Returns: Returns:
- title: a string containing the PR title. - title: a string containing the PR title.
- pr_body: a string containing the PR body in a markdown format. - pr_body: a string containing the PR description body in a markdown format.
- pr_types: a list of strings containing the PR types.
- markdown_text: a string containing the AI prediction data in a markdown format. used for publishing a comment
""" """
# Load the AI prediction data into a dictionary
data = load_yaml(self.prediction.strip())
if get_settings().pr_description.add_original_user_description and self.user_description: # Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
data["User Description"] = self.user_description markdown_text = ""
# Don't display 'PR Labels'
# Initialization if 'labels' in self.data and self.git_provider.is_supported("get_labels"):
pr_types = [] self.data.pop('labels')
if not get_settings().pr_description.enable_pr_type:
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types' self.data.pop('type')
if 'PR Type' in data: for key, value in self.data.items():
if type(data['PR Type']) == list: markdown_text += f"## {key}\n\n"
pr_types = data['PR Type'] markdown_text += f"{value}\n\n"
elif type(data['PR Type']) == str:
pr_types = data['PR Type'].split(',')
# Remove the 'PR Title' key from the dictionary # Remove the 'PR Title' key from the dictionary
ai_title = data.pop('PR 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"]
@ -172,25 +266,132 @@ class PRDescription:
# Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format, # Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format,
# except for the items containing the word 'walkthrough' # except for the items containing the word 'walkthrough'
pr_body = "" pr_body = ""
for idx, (key, value) in enumerate(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"):
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"):
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(data) - 1: if idx < len(self.data) - 1:
pr_body += "\n___\n" pr_body += "\n___\n"
markdown_text = f"## Title\n\n{title}\n\n___\n{pr_body}"
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"title:\n{title}\n{pr_body}") get_logger().info(f"title:\n{title}\n{pr_body}")
return title, pr_body, pr_types, markdown_text 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

@ -0,0 +1,171 @@
import copy
import re
from typing import List, Tuple
from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRGenerateLabels:
def __init__(self, pr_url: str, args: list = None):
"""
Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels
corresponding to the PR using an AI model.
Args:
pr_url (str): The URL of the pull request.
args (list, optional): List of arguments passed to the PRGenerateLabels class. Defaults to None.
"""
# Initialize the git provider and main PR language
self.git_provider = get_git_provider()(pr_url)
self.main_pr_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self.pr_id = self.git_provider.get_pr_id()
# Initialize the AI handler
self.ai_handler = AiHandler()
# Initialize the variables dictionary
self.vars = {
"title": self.git_provider.pr.title,
"branch": self.git_provider.get_pr_branch(),
"description": self.git_provider.get_pr_description(full=False),
"language": self.main_pr_language,
"diff": "", # empty diff for initial calculation
"use_bullet_points": get_settings().pr_description.use_bullet_points,
"extra_instructions": get_settings().pr_description.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(),
"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
self.token_handler = TokenHandler(
self.git_provider.pr,
self.vars,
get_settings().pr_custom_labels_prompt.system,
get_settings().pr_custom_labels_prompt.user,
)
# Initialize patches_diff and prediction attributes
self.patches_diff = None
self.prediction = None
async def run(self):
"""
Generates a PR labels using an AI model and publishes it to the PR.
"""
try:
get_logger().info(f"Generating a PR labels {self.pr_id}")
if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing PR labels...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction)
get_logger().info(f"Preparing answer {self.pr_id}")
if self.prediction:
self._prepare_data()
else:
return None
pr_labels = self._prepare_labels()
if get_settings().config.publish_output:
get_logger().info(f"Pushing labels {self.pr_id}")
current_labels = self.git_provider.get_labels()
user_labels = get_user_labels(current_labels)
pr_labels = pr_labels + user_labels
if self.git_provider.is_supported("get_labels"):
self.git_provider.publish_labels(pr_labels)
elif pr_labels:
value = ', '.join(v for v in pr_labels)
pr_labels_text = f"## PR Labels:\n{value}\n"
self.git_provider.publish_comment(pr_labels_text, is_temporary=False)
self.git_provider.remove_initial_comment()
except Exception as e:
get_logger().error(f"Error generating PR labels {self.pr_id}: {e}")
return ""
async def _prepare_prediction(self, model: str) -> None:
"""
Prepare the AI prediction for the PR labels based on the provided model.
Args:
model (str): The name of the model to be used for generating the prediction.
Returns:
None
Raises:
Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions.
"""
get_logger().info(f"Getting PR diff {self.pr_id}")
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
get_logger().info(f"Getting AI prediction {self.pr_id}")
self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str) -> str:
"""
Generate an AI prediction for the PR labels based on the provided model.
Args:
model (str): The name of the model to be used for generating the prediction.
Returns:
str: The generated AI prediction.
"""
variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined)
set_custom_labels(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)
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(
model=model,
temperature=0.2,
system=system_prompt,
user=user_prompt
)
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nAI response:\n{response}")
return response
def _prepare_data(self):
# Load the AI prediction data into a dictionary
self.data = load_yaml(self.prediction.strip())
def _prepare_labels(self) -> List[str]:
pr_types = []
# If the 'labels' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'labels' in self.data:
if type(self.data['labels']) == list:
pr_types = self.data['labels']
elif type(self.data['labels']) == str:
pr_types = self.data['labels'].split(',')
return pr_types

View File

@ -1,5 +1,4 @@
import copy import copy
import logging
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
@ -9,6 +8,7 @@ from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRInformationFromUser: class PRInformationFromUser:
@ -34,22 +34,22 @@ class PRInformationFromUser:
self.prediction = None self.prediction = None
async def run(self): async def run(self):
logging.info('Generating question to the user...') get_logger().info('Generating question to the user...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing questions...", is_temporary=True) self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
logging.info('Preparing questions...') get_logger().info('Preparing questions...')
pr_comment = self._prepare_pr_answer() pr_comment = self._prepare_pr_answer()
if get_settings().config.publish_output: if get_settings().config.publish_output:
logging.info('Pushing questions...') get_logger().info('Pushing questions...')
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
return "" return ""
async def _prepare_prediction(self, model): async def _prepare_prediction(self, model):
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
logging.info('Getting AI prediction...') get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str): async def _get_prediction(self, model: str):
@ -59,8 +59,8 @@ class PRInformationFromUser:
system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt) system=system_prompt, user=user_prompt)
return response return response
@ -68,7 +68,7 @@ class PRInformationFromUser:
def _prepare_pr_answer(self) -> str: def _prepare_pr_answer(self) -> str:
model_output = self.prediction.strip() model_output = self.prediction.strip()
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"answer_str:\n{model_output}") get_logger().info(f"answer_str:\n{model_output}")
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\ answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
"\n>/answer\n>1) ...\n>2) ...\n>...\n" "\n>/answer\n>1) ...\n>2) ...\n>...\n"
return answer_str return answer_str

View File

@ -1,5 +1,4 @@
import copy import copy
import logging
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
@ -9,6 +8,7 @@ from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
class PRQuestions: class PRQuestions:
@ -44,22 +44,22 @@ class PRQuestions:
return question_str return question_str
async def run(self): async def run(self):
logging.info('Answering a PR question...') get_logger().info('Answering a PR question...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing answer...", is_temporary=True) self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
logging.info('Preparing answer...') get_logger().info('Preparing answer...')
pr_comment = self._prepare_pr_answer() pr_comment = self._prepare_pr_answer()
if get_settings().config.publish_output: if get_settings().config.publish_output:
logging.info('Pushing answer...') get_logger().info('Pushing answer...')
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
return "" return ""
async def _prepare_prediction(self, model: str): async def _prepare_prediction(self, model: str):
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
logging.info('Getting AI prediction...') get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str): async def _get_prediction(self, model: str):
@ -69,8 +69,8 @@ class PRQuestions:
system_prompt = environment.from_string(get_settings().pr_questions_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_questions_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt) system=system_prompt, user=user_prompt)
return response return response
@ -79,5 +79,5 @@ class PRQuestions:
answer_str = f"Question: {self.question_str}\n\n" answer_str = f"Question: {self.question_str}\n\n"
answer_str += f"Answer:\n{self.prediction.strip()}\n\n" answer_str += f"Answer:\n{self.prediction.strip()}\n\n"
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"answer_str:\n{answer_str}") get_logger().info(f"answer_str:\n{answer_str}")
return answer_str return answer_str

View File

@ -1,6 +1,5 @@
import copy import copy
import json import datetime
import logging
from collections import OrderedDict from collections import OrderedDict
from typing import List, Tuple from typing import List, Tuple
@ -9,13 +8,13 @@ from jinja2 import Environment, StrictUndefined
from yaml import SafeLoader from yaml import SafeLoader
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, \ from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
find_line_number_of_relevant_line_in_file, clip_tokens
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import convert_to_markdown, try_fix_json, try_fix_yaml, load_yaml from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml, set_custom_labels, get_user_labels
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language
from pr_agent.log import get_logger
from pr_agent.servers.help import actions_help_text, bot_help_text from pr_agent.servers.help import actions_help_text, bot_help_text
@ -59,11 +58,14 @@ class PRReviewer:
"require_tests": get_settings().pr_reviewer.require_tests_review, "require_tests": get_settings().pr_reviewer.require_tests_review,
"require_security": get_settings().pr_reviewer.require_security_review, "require_security": get_settings().pr_reviewer.require_security_review,
"require_focused": get_settings().pr_reviewer.require_focused_review, "require_focused": get_settings().pr_reviewer.require_focused_review,
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
'num_code_suggestions': get_settings().pr_reviewer.num_code_suggestions, 'num_code_suggestions': get_settings().pr_reviewer.num_code_suggestions,
'question_str': question_str, 'question_str': question_str,
'answer_str': answer_str, 'answer_str': answer_str,
"extra_instructions": get_settings().pr_reviewer.extra_instructions, "extra_instructions": get_settings().pr_reviewer.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
"custom_labels": "",
"enable_custom_labels": get_settings().config.enable_custom_labels,
} }
self.token_handler = TokenHandler( self.token_handler = TokenHandler(
@ -94,28 +96,44 @@ class PRReviewer:
""" """
Review the pull request and generate feedback. Review the pull request and generate feedback.
""" """
try:
if self.is_auto and not get_settings().pr_reviewer.automatic_review: if self.is_auto and not get_settings().pr_reviewer.automatic_review:
logging.info(f'Automatic review is disabled {self.pr_url}') get_logger().info(f'Automatic review is disabled {self.pr_url}')
return None
if self.incremental.is_incremental and not self._can_run_incremental_review():
return None return None
logging.info(f'Reviewing PR: {self.pr_url} ...') get_logger().info(f'Reviewing PR: {self.pr_url} ...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
logging.info('Preparing PR review...') get_logger().info('Preparing PR review...')
pr_comment = self._prepare_pr_review() pr_comment = self._prepare_pr_review()
if get_settings().config.publish_output: if get_settings().config.publish_output:
logging.info('Pushing PR review...') get_logger().info('Pushing PR review...')
self.git_provider.publish_comment(pr_comment) previous_review_comment = self._get_previous_review_comment()
self.git_provider.remove_initial_comment()
# publish the review
if get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental:
self.git_provider.publish_persistent_comment(pr_comment,
initial_header="## PR Analysis",
update_header=True)
else:
self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment()
if previous_review_comment:
self._remove_previous_review_comment(previous_review_comment)
if get_settings().pr_reviewer.inline_code_comments: if get_settings().pr_reviewer.inline_code_comments:
logging.info('Pushing inline code comments...') get_logger().info('Pushing inline code comments...')
self._publish_inline_code_comments() self._publish_inline_code_comments()
except Exception as e:
get_logger().error(f"Failed to review PR: {e}")
async def _prepare_prediction(self, model: str) -> None: async def _prepare_prediction(self, model: str) -> None:
""" """
@ -127,9 +145,9 @@ class PRReviewer:
Returns: Returns:
None None
""" """
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
logging.info('Getting AI prediction...') get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str) -> str: async def _get_prediction(self, model: str) -> str:
@ -150,8 +168,8 @@ class PRReviewer:
user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion( response, finish_reason = await self.ai_handler.chat_completion(
model=model, model=model,
@ -160,6 +178,9 @@ class PRReviewer:
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_pr_review(self) -> str: def _prepare_pr_review(self) -> str:
@ -204,30 +225,46 @@ class PRReviewer:
link = self.git_provider.generate_link_to_relevant_line_number(suggestion) link = self.git_provider.generate_link_to_relevant_line_number(suggestion)
if link: if link:
suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})" suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})"
else:
pass
# Add incremental review section # Add incremental review section
if self.incremental.is_incremental: if self.incremental.is_incremental:
last_commit_url = f"{self.git_provider.get_pr_url()}/commits/" \ last_commit_url = f"{self.git_provider.get_pr_url()}/commits/" \
f"{self.git_provider.incremental.first_new_commit_sha}" f"{self.git_provider.incremental.first_new_commit_sha}"
last_commit_msg = self.incremental.commits_range[0].commit.message if self.incremental.commits_range else ""
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
if last_commit_msg:
replacement = last_commit_msg.splitlines(keepends=False)[0].replace('_', r'\_')
incremental_review_markdown_text += f" \n_({replacement})_"
data = OrderedDict(data) data = OrderedDict(data)
data.update({'Incremental PR Review': { data.update({'Incremental PR Review': {
"⏮️ Review for commits since previous PR-Agent review": f"Starting from commit {last_commit_url}"}}) "⏮️ Review for commits since previous PR-Agent review": incremental_review_markdown_text}})
data.move_to_end('Incremental PR Review', last=False) data.move_to_end('Incremental PR Review', last=False)
markdown_text = convert_to_markdown(data) markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
user = self.git_provider.get_user_id() user = self.git_provider.get_user_id()
# 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 user and '[bot]' not in user: 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
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)
self.set_review_labels(data)
# Log markdown response if verbosity level is high # Log markdown response if verbosity level is high
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"Markdown response:\n{markdown_text}") get_logger().info(f"Markdown response:\n{markdown_text}")
if markdown_text == None or len(markdown_text) == 0: if markdown_text == None or len(markdown_text) == 0:
markdown_text = "" markdown_text = ""
@ -241,21 +278,14 @@ 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:
logging.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()
relevant_line_in_file = suggestion.get('relevant line', '').strip() relevant_line_in_file = suggestion.get('relevant line', '').strip()
content = suggestion.get('suggestion', '') content = suggestion.get('suggestion', '')
if not relevant_file or not relevant_line_in_file or not content: if not relevant_file or not relevant_line_in_file or not content:
logging.info("Skipping inline comment with missing file/line/content") get_logger().info("Skipping inline comment with missing file/line/content")
continue continue
if self.git_provider.is_supported("create_inline_comment"): if self.git_provider.is_supported("create_inline_comment"):
@ -291,3 +321,83 @@ class PRReviewer:
break break
return question_str, answer_str return question_str, answer_str
def _get_previous_review_comment(self):
"""
Get the previous review comment if it exists.
"""
try:
if get_settings().pr_reviewer.remove_previous_review_comment and hasattr(self.git_provider, "get_previous_review"):
return self.git_provider.get_previous_review(
full=not self.incremental.is_incremental,
incremental=self.incremental.is_incremental,
)
except Exception as e:
get_logger().exception(f"Failed to get previous review comment, error: {e}")
def _remove_previous_review_comment(self, comment):
"""
Remove the previous review comment if it exists.
"""
try:
if get_settings().pr_reviewer.remove_previous_review_comment and comment:
self.git_provider.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove previous review comment, error: {e}")
def _can_run_incremental_review(self) -> bool:
"""Checks if we can run incremental review according the various configurations and previous review"""
# checking if running is auto mode but there are no new commits
if self.is_auto and not self.incremental.first_new_commit_sha:
get_logger().info(f"Incremental review is enabled for {self.pr_url} but there are no new commits")
return False
# checking if there are enough commits to start the review
num_new_commits = len(self.incremental.commits_range)
num_commits_threshold = get_settings().pr_reviewer.minimal_commits_for_incremental_review
not_enough_commits = num_new_commits < num_commits_threshold
# checking if the commits are not too recent to start the review
recent_commits_threshold = datetime.datetime.now() - datetime.timedelta(
minutes=get_settings().pr_reviewer.minimal_minutes_for_incremental_review
)
last_seen_commit_date = (
self.incremental.last_seen_commit.commit.author.date if self.incremental.last_seen_commit else None
)
all_commits_too_recent = (
last_seen_commit_date > recent_commits_threshold if self.incremental.last_seen_commit else False
)
# check all the thresholds or just one to start the review
condition = any if get_settings().pr_reviewer.require_all_thresholds_for_incremental_review else all
if condition((not_enough_commits, all_commits_too_recent)):
get_logger().info(
f"Incremental review is enabled for {self.pr_url} but didn't pass the threshold check to run:"
f"\n* Number of new commits = {num_new_commits} (threshold is {num_commits_threshold})"
f"\n* Last seen commit date = {last_seen_commit_date} (threshold is {recent_commits_threshold})"
)
return False
return True
def set_review_labels(self, data):
if (get_settings().pr_reviewer.enable_review_labels_security or
get_settings().pr_reviewer.enable_review_labels_effort):
try:
review_labels = []
if get_settings().pr_reviewer.enable_review_labels_effort:
estimated_effort = data['PR Analysis']['Estimated effort to review [1-5]']
estimated_effort_number = int(estimated_effort.split(',')[0])
if 1 <= estimated_effort_number <= 5: # 1, because ...
review_labels.append(f'Review effort [1-5]: {estimated_effort_number}')
if get_settings().pr_reviewer.enable_review_labels_security:
security_concerns = data['PR Analysis']['Security concerns'] # yes, because ...
security_concerns_bool = 'yes' in security_concerns.lower() or 'true' in security_concerns.lower()
if security_concerns_bool:
review_labels.append('Possible security concern')
current_labels = self.git_provider.get_labels()
current_labels_filtered = [label for label in current_labels if
not label.lower().startswith('review effort [1-5]:') and not label.lower().startswith(
'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)
except Exception as e:
get_logger().error(f"Failed to set review labels, error: {e}")

View File

@ -0,0 +1,295 @@
import time
from enum import Enum
from typing import List
import openai
import pandas as pd
import pinecone
from pinecone_datasets import Dataset, DatasetMetadata
from pydantic import BaseModel, Field
from pr_agent.algo import MAX_TOKENS
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.log import get_logger
MODEL = "text-embedding-ada-002"
class PRSimilarIssue:
def __init__(self, issue_url: str, args: list = None):
if get_settings().config.git_provider != "github":
raise Exception("Only github is supported for similar issue tool")
self.cli_mode = get_settings().CONFIG.CLI_MODE
self.max_issues_to_scan = get_settings().pr_similar_issue.max_issues_to_scan
self.issue_url = issue_url
self.git_provider = get_git_provider()()
repo_name, issue_number = self.git_provider._parse_issue_url(issue_url.split('=')[-1])
self.git_provider.repo = repo_name
self.git_provider.repo_obj = self.git_provider.github_client.get_repo(repo_name)
self.token_handler = TokenHandler()
repo_obj = self.git_provider.repo_obj
repo_name_for_index = self.repo_name_for_index = repo_obj.full_name.lower().replace('/', '-').replace('_/', '-')
index_name = self.index_name = "codium-ai-pr-agent-issues"
# assuming pinecone api key and environment are set in secrets file
try:
api_key = get_settings().pinecone.api_key
environment = get_settings().pinecone.environment
except Exception:
if not self.cli_mode:
repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1])
issue_main = self.git_provider.repo_obj.get_issue(original_issue_number)
issue_main.create_comment("Please set pinecone api key and environment in secrets file")
raise Exception("Please set pinecone api key and environment in secrets file")
# check if index exists, and if repo is already indexed
run_from_scratch = False
if run_from_scratch: # for debugging
pinecone.init(api_key=api_key, environment=environment)
if index_name in pinecone.list_indexes():
get_logger().info('Removing index...')
pinecone.delete_index(index_name)
get_logger().info('Done')
upsert = True
pinecone.init(api_key=api_key, environment=environment)
if not index_name in pinecone.list_indexes():
run_from_scratch = True
upsert = False
else:
if get_settings().pr_similar_issue.force_update_dataset:
upsert = True
else:
pinecone_index = pinecone.Index(index_name=index_name)
res = pinecone_index.fetch([f"example_issue_{repo_name_for_index}"]).to_dict()
if res["vectors"]:
upsert = False
if run_from_scratch or upsert: # index the entire repo
get_logger().info('Indexing the entire repo...')
get_logger().info('Getting issues...')
issues = list(repo_obj.get_issues(state='all'))
get_logger().info('Done')
self._update_index_with_issues(issues, repo_name_for_index, upsert=upsert)
else: # update index if needed
pinecone_index = pinecone.Index(index_name=index_name)
issues_to_update = []
issues_paginated_list = repo_obj.get_issues(state='all')
counter = 1
for issue in issues_paginated_list:
if issue.pull_request:
continue
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}"
id = issue_key + "." + "issue"
res = pinecone_index.fetch([id]).to_dict()
is_new_issue = True
for vector in res["vectors"].values():
if vector['metadata']['repo'] == repo_name_for_index:
is_new_issue = False
break
if is_new_issue:
counter += 1
issues_to_update.append(issue)
else:
break
if issues_to_update:
get_logger().info(f'Updating index with {counter} new issues...')
self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True)
else:
get_logger().info('No new issues to update')
async def run(self):
get_logger().info('Getting issue...')
repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1])
issue_main = self.git_provider.repo_obj.get_issue(original_issue_number)
issue_str, comments, number = self._process_issue(issue_main)
openai.api_key = get_settings().openai.key
get_logger().info('Done')
get_logger().info('Querying...')
res = openai.Embedding.create(input=[issue_str], engine=MODEL)
embeds = [record['embedding'] for record in res['data']]
pinecone_index = pinecone.Index(index_name=self.index_name)
res = pinecone_index.query(embeds[0],
top_k=5,
filter={"repo": self.repo_name_for_index},
include_metadata=True).to_dict()
relevant_issues_number_list = []
relevant_comment_number_list = []
score_list = []
for r in res['matches']:
# skip example issue
if 'example_issue_' in r["id"]:
continue
try:
issue_number = int(r["id"].split('.')[0].split('_')[-1])
except:
get_logger().debug(f"Failed to parse issue number from {r['id']}")
continue
if original_issue_number == issue_number:
continue
if issue_number not in relevant_issues_number_list:
relevant_issues_number_list.append(issue_number)
if 'comment' in r["id"]:
relevant_comment_number_list.append(int(r["id"].split('.')[1].split('_')[-1]))
else:
relevant_comment_number_list.append(-1)
score_list.append(str("{:.2f}".format(r['score'])))
get_logger().info('Done')
get_logger().info('Publishing response...')
similar_issues_str = "### Similar Issues\n___\n\n"
for i, issue_number_similar in enumerate(relevant_issues_number_list):
issue = self.git_provider.repo_obj.get_issue(issue_number_similar)
title = issue.title
url = issue.html_url
if relevant_comment_number_list[i] != -1:
url = list(issue.get_comments())[relevant_comment_number_list[i]].html_url
similar_issues_str += f"{i + 1}. **[{title}]({url})** (score={score_list[i]})\n\n"
if get_settings().config.publish_output:
response = issue_main.create_comment(similar_issues_str)
get_logger().info(similar_issues_str)
get_logger().info('Done')
def _process_issue(self, issue):
header = issue.title
body = issue.body
number = issue.number
if get_settings().pr_similar_issue.skip_comments:
comments = []
else:
comments = list(issue.get_comments())
issue_str = f"Issue Header: \"{header}\"\n\nIssue Body:\n{body}"
return issue_str, comments, number
def _update_index_with_issues(self, issues_list, repo_name_for_index, upsert=False):
get_logger().info('Processing issues...')
corpus = Corpus()
example_issue_record = Record(
id=f"example_issue_{repo_name_for_index}",
text="example_issue",
metadata=Metadata(repo=repo_name_for_index)
)
corpus.append(example_issue_record)
counter = 0
for issue in issues_list:
if issue.pull_request:
continue
counter += 1
if counter % 100 == 0:
get_logger().info(f"Scanned {counter} issues")
if counter >= self.max_issues_to_scan:
get_logger().info(f"Scanned {self.max_issues_to_scan} issues, stopping")
break
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}"
username = issue.user.login
created_at = str(issue.created_at)
if len(issue_str) < 8000 or \
self.token_handler.count_tokens(issue_str) < get_max_tokens(MODEL): # fast reject first
issue_record = Record(
id=issue_key + "." + "issue",
text=issue_str,
metadata=Metadata(repo=repo_name_for_index,
username=username,
created_at=created_at,
level=IssueLevel.ISSUE)
)
corpus.append(issue_record)
if comments:
for j, comment in enumerate(comments):
comment_body = comment.body
num_words_comment = len(comment_body.split())
if num_words_comment < 10 or not isinstance(comment_body, str):
continue
if len(comment_body) < 8000 or \
self.token_handler.count_tokens(comment_body) < MAX_TOKENS[MODEL]:
comment_record = Record(
id=issue_key + ".comment_" + str(j + 1),
text=comment_body,
metadata=Metadata(repo=repo_name_for_index,
username=username, # use issue username for all comments
created_at=created_at,
level=IssueLevel.COMMENT)
)
corpus.append(comment_record)
df = pd.DataFrame(corpus.dict()["documents"])
get_logger().info('Done')
get_logger().info('Embedding...')
openai.api_key = get_settings().openai.key
list_to_encode = list(df["text"].values)
try:
res = openai.Embedding.create(input=list_to_encode, engine=MODEL)
embeds = [record['embedding'] for record in res['data']]
except:
embeds = []
get_logger().error('Failed to embed entire list, embedding one by one...')
for i, text in enumerate(list_to_encode):
try:
res = openai.Embedding.create(input=[text], engine=MODEL)
embeds.append(res['data'][0]['embedding'])
except:
embeds.append([0] * 1536)
df["values"] = embeds
meta = DatasetMetadata.empty()
meta.dense_model.dimension = len(embeds[0])
ds = Dataset.from_pandas(df, meta)
get_logger().info('Done')
api_key = get_settings().pinecone.api_key
environment = get_settings().pinecone.environment
if not upsert:
get_logger().info('Creating index from scratch...')
ds.to_pinecone_index(self.index_name, api_key=api_key, environment=environment)
time.sleep(15) # wait for pinecone to finalize indexing before querying
else:
get_logger().info('Upserting index...')
namespace = ""
batch_size: int = 100
concurrency: int = 10
pinecone.init(api_key=api_key, environment=environment)
ds._upsert_to_index(self.index_name, namespace, batch_size, concurrency)
time.sleep(5) # wait for pinecone to finalize upserting before querying
get_logger().info('Done')
class IssueLevel(str, Enum):
ISSUE = "issue"
COMMENT = "comment"
class Metadata(BaseModel):
repo: str
username: str = Field(default="@codium")
created_at: str = Field(default="01-01-1970 00:00:00.00000")
level: IssueLevel = Field(default=IssueLevel.ISSUE)
class Config:
use_enum_values = True
class Record(BaseModel):
id: str
text: str
metadata: Metadata
class Corpus(BaseModel):
documents: List[Record] = Field(default=[])
def append(self, r: Record):
self.documents.append(r)

View File

@ -1,5 +1,4 @@
import copy import copy
import logging
from datetime import date from datetime import date
from time import sleep from time import sleep
from typing import Tuple from typing import Tuple
@ -10,8 +9,9 @@ from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers import GithubProvider, get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.log import get_logger
CHANGELOG_LINES = 50 CHANGELOG_LINES = 50
@ -46,28 +46,28 @@ class PRUpdateChangelog:
get_settings().pr_update_changelog_prompt.user) get_settings().pr_update_changelog_prompt.user)
async def run(self): async def run(self):
assert type(self.git_provider) == GithubProvider, "Currently only Github is supported" # assert type(self.git_provider) == GithubProvider, "Currently only Github is supported"
logging.info('Updating the changelog...') get_logger().info('Updating the changelog...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True) self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
logging.info('Preparing PR changelog updates...') get_logger().info('Preparing PR changelog updates...')
new_file_content, answer = self._prepare_changelog_update() new_file_content, answer = self._prepare_changelog_update()
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
logging.info('Publishing changelog updates...') get_logger().info('Publishing changelog updates...')
if self.commit_changelog: if self.commit_changelog:
logging.info('Pushing PR changelog updates to repo...') get_logger().info('Pushing PR changelog updates to repo...')
self._push_changelog_update(new_file_content, answer) self._push_changelog_update(new_file_content, answer)
else: else:
logging.info('Publishing PR changelog as comment...') get_logger().info('Publishing PR changelog as comment...')
self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}") self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}")
async def _prepare_prediction(self, model: str): async def _prepare_prediction(self, model: str):
logging.info('Getting PR diff...') get_logger().info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
logging.info('Getting AI prediction...') get_logger().info('Getting AI prediction...')
self.prediction = await self._get_prediction(model) self.prediction = await self._get_prediction(model)
async def _get_prediction(self, model: str): async def _get_prediction(self, model: str):
@ -77,8 +77,8 @@ class PRUpdateChangelog:
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}") get_logger().info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}") get_logger().info(f"\nUser prompt:\n{user_prompt}")
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt) system=system_prompt, user=user_prompt)
@ -100,7 +100,7 @@ class PRUpdateChangelog:
"\n>'/update_changelog --pr_update_changelog.push_changelog_changes=true'\n" "\n>'/update_changelog --pr_update_changelog.push_changelog_changes=true'\n"
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
logging.info(f"answer:\n{answer}") get_logger().info(f"answer:\n{answer}")
return new_file_content, answer return new_file_content, answer
@ -149,7 +149,7 @@ Example:
except Exception: except Exception:
self.changelog_file_str = "" self.changelog_file_str = ""
if self.commit_changelog: if self.commit_changelog:
logging.info("No CHANGELOG.md file found in the repository. Creating one...") get_logger().info("No CHANGELOG.md file found in the repository. Creating one...")
changelog_file = self.git_provider.repo_obj.create_file(path="CHANGELOG.md", changelog_file = self.git_provider.repo_obj.create_file(path="CHANGELOG.md",
message='add CHANGELOG.md', message='add CHANGELOG.md',
content="", content="",

View File

@ -1,19 +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 azure-devops==7.1.0b3
PyYAML==6.0 boto3==1.33.6
starlette-context==0.3.6 dynaconf==3.2.4
litellm~=0.1.445 fastapi==0.99.0
boto3~=1.28.25 GitPython==3.1.32
google-cloud-aiplatform==1.35.0
google-cloud-storage==2.10.0 google-cloud-storage==2.10.0
Jinja2==3.1.2
litellm==0.12.5
loguru==0.7.2
msrest==0.7.1
openai==0.27.8
pinecone-client
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
pytest==7.4.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 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

@ -110,7 +110,7 @@ class TestCodeCommitProvider:
# Mock the response from the AWS client for get_pull_request method # Mock the response from the AWS client for get_pull_request method
api.boto_client.get_pull_request.return_value = { api.boto_client.get_pull_request.return_value = {
"pullRequest": { "pullRequest": {
"pullRequestId": "3", "pullRequestId": "321",
"title": "My PR", "title": "My PR",
"description": "My PR description", "description": "My PR description",
"pullRequestTargets": [ "pullRequestTargets": [
@ -125,7 +125,7 @@ class TestCodeCommitProvider:
} }
} }
pr = api.get_pr(321) pr = api.get_pr("my_test_repo", 321)
assert pr.title == "My PR" assert pr.title == "My PR"
assert pr.description == "My PR description" assert pr.description == "My PR description"

View File

@ -1,6 +1,8 @@
import pytest import pytest
from unittest.mock import patch
from pr_agent.git_providers.codecommit_provider import CodeCommitFile from pr_agent.git_providers.codecommit_provider import CodeCommitFile
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
from pr_agent.git_providers.codecommit_provider import PullRequestCCMimic
from pr_agent.git_providers.git_provider import EDIT_TYPE from pr_agent.git_providers.git_provider import EDIT_TYPE
@ -25,12 +27,64 @@ class TestCodeCommitFile:
class TestCodeCommitProvider: class TestCodeCommitProvider:
def test_get_title(self):
# Test that the get_title() function returns the PR title
with patch.object(CodeCommitProvider, "__init__", lambda x, y: None):
provider = CodeCommitProvider(None)
provider.pr = PullRequestCCMimic("My Test PR Title", [])
assert provider.get_title() == "My Test PR Title"
def test_get_pr_id(self):
# Test that the get_pr_id() function returns the correct ID
with patch.object(CodeCommitProvider, "__init__", lambda x, y: None):
provider = CodeCommitProvider(None)
provider.repo_name = "my_test_repo"
provider.pr_num = 321
assert provider.get_pr_id() == "my_test_repo/321"
def test_parse_pr_url(self): def test_parse_pr_url(self):
# Test that the _parse_pr_url() function can extract the repo name and PR number from a CodeCommit URL
url = "https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my_test_repo/pull-requests/321" url = "https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my_test_repo/pull-requests/321"
repo_name, pr_number = CodeCommitProvider._parse_pr_url(url) repo_name, pr_number = CodeCommitProvider._parse_pr_url(url)
assert repo_name == "my_test_repo" assert repo_name == "my_test_repo"
assert pr_number == 321 assert pr_number == 321
def test_is_valid_codecommit_hostname(self):
# Test the various AWS regions
assert CodeCommitProvider._is_valid_codecommit_hostname("af-south-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-east-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-3.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-3.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-4.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("ca-central-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-north-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-3.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("il-central-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("me-central-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("me-south-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("sa-east-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-2.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-east-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-west-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-1.console.aws.amazon.com")
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-2.console.aws.amazon.com")
# Test non-AWS regions
assert not CodeCommitProvider._is_valid_codecommit_hostname("no-such-region.console.aws.amazon.com")
assert not CodeCommitProvider._is_valid_codecommit_hostname("console.aws.amazon.com")
# Test that an error is raised when an invalid CodeCommit URL is provided to the set_pr() method of the CodeCommitProvider class. # Test that an error is raised when an invalid CodeCommit URL is provided to the set_pr() method of the CodeCommitProvider class.
# Generated by CodiumAI # Generated by CodiumAI
def test_invalid_codecommit_url(self): def test_invalid_codecommit_url(self):
@ -106,6 +160,7 @@ class TestCodeCommitProvider:
assert percentages == {} assert percentages == {}
def test_get_edit_type(self): def test_get_edit_type(self):
# Test that the _get_edit_type() function can convert a CodeCommit letter to an EDIT_TYPE enum
assert CodeCommitProvider._get_edit_type("A") == EDIT_TYPE.ADDED assert CodeCommitProvider._get_edit_type("A") == EDIT_TYPE.ADDED
assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED
assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED
@ -117,3 +172,18 @@ class TestCodeCommitProvider:
assert CodeCommitProvider._get_edit_type("r") == EDIT_TYPE.RENAMED assert CodeCommitProvider._get_edit_type("r") == EDIT_TYPE.RENAMED
assert CodeCommitProvider._get_edit_type("X") is None assert CodeCommitProvider._get_edit_type("X") is None
def test_add_additional_newlines(self):
# a short string to test adding double newlines
input = "abc\ndef\n\n___\nghi\njkl\nmno\n\npqr\n"
expect = "abc\n\ndef\n\n___\n\nghi\n\njkl\n\nmno\n\npqr\n\n"
assert CodeCommitProvider._add_additional_newlines(input) == expect
# a test example from a real PR
input = "## PR Type:\nEnhancement\n\n___\n## PR Description:\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n## PR Main Files Walkthrough:\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n`bar`: The bar script has been updated to list stopped servers.\n"
expect = "## PR Type:\n\nEnhancement\n\n___\n\n## PR Description:\n\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n\n## PR Main Files Walkthrough:\n\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n\n`bar`: The bar script has been updated to list stopped servers.\n\n"
assert CodeCommitProvider._add_additional_newlines(input) == expect
def test_remove_markdown_html(self):
input = "## PR Feedback\n<details><summary>Code feedback:</summary>\nfile foo\n</summary>\n"
expect = "## PR Feedback\nCode feedback:\nfile foo\n\n"
assert CodeCommitProvider._remove_markdown_html(input) == expect

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>\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>\
""" """
assert convert_to_markdown(input_data).strip() == expected_output.strip() assert convert_to_markdown(input_data).strip() == expected_output.strip()

View File

@ -0,0 +1,80 @@
import pytest
from pr_agent.algo.file_filter import filter_ignored
from pr_agent.config_loader import global_settings
class TestIgnoreFilter:
def test_no_ignores(self):
"""
Test no files are ignored when no patterns are specified.
"""
files = [
type('', (object,), {'filename': 'file1.py'})(),
type('', (object,), {'filename': 'file2.java'})(),
type('', (object,), {'filename': 'file3.cpp'})(),
type('', (object,), {'filename': 'file4.py'})(),
type('', (object,), {'filename': 'file5.py'})()
]
assert filter_ignored(files) == files, "Expected all files to be returned when no ignore patterns are given."
def test_glob_ignores(self, monkeypatch):
"""
Test files are ignored when glob patterns are specified.
"""
monkeypatch.setattr(global_settings.ignore, 'glob', ['*.py'])
files = [
type('', (object,), {'filename': 'file1.py'})(),
type('', (object,), {'filename': 'file2.java'})(),
type('', (object,), {'filename': 'file3.cpp'})(),
type('', (object,), {'filename': 'file4.py'})(),
type('', (object,), {'filename': 'file5.py'})()
]
expected = [
files[1],
files[2]
]
filtered_files = filter_ignored(files)
assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}."
def test_regex_ignores(self, monkeypatch):
"""
Test files are ignored when regex patterns are specified.
"""
monkeypatch.setattr(global_settings.ignore, 'regex', ['^file[2-4]\..*$'])
files = [
type('', (object,), {'filename': 'file1.py'})(),
type('', (object,), {'filename': 'file2.java'})(),
type('', (object,), {'filename': 'file3.cpp'})(),
type('', (object,), {'filename': 'file4.py'})(),
type('', (object,), {'filename': 'file5.py'})()
]
expected = [
files[0],
files[4]
]
filtered_files = filter_ignored(files)
assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}."
def test_invalid_regex(self, monkeypatch):
"""
Test invalid patterns are quietly ignored.
"""
monkeypatch.setattr(global_settings.ignore, 'regex', ['(((||', '^file[2-4]\..*$'])
files = [
type('', (object,), {'filename': 'file1.py'})(),
type('', (object,), {'filename': 'file2.java'})(),
type('', (object,), {'filename': 'file3.cpp'})(),
type('', (object,), {'filename': 'file4.py'})(),
type('', (object,), {'filename': 'file5.py'})()
]
expected = [
files[0],
files[4]
]
filtered_files = filter_ignored(files)
assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}."

View File

@ -43,18 +43,6 @@ class TestHandlePatchDeletions:
assert handle_patch_deletions(patch, original_file_content_str, new_file_content_str, assert handle_patch_deletions(patch, original_file_content_str, new_file_content_str,
file_name) == patch.rstrip() file_name) == patch.rstrip()
# Tests that handle_patch_deletions logs a message when verbosity_level is greater than 0
def test_handle_patch_deletions_happy_path_verbosity_level_greater_than_0(self, caplog):
patch = '--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n-bar\n+baz\n'
original_file_content_str = 'foo\nbar\n'
new_file_content_str = ''
file_name = 'file.py'
get_settings().config.verbosity_level = 1
with caplog.at_level(logging.INFO):
handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file_name)
assert any("Processing file" in message for message in caplog.messages)
# Tests that handle_patch_deletions returns 'File was deleted' when new_file_content_str is empty # Tests that handle_patch_deletions returns 'File was deleted' when new_file_content_str is empty
def test_handle_patch_deletions_edge_case_new_file_content_empty(self): def test_handle_patch_deletions_edge_case_new_file_content_empty(self):
patch = '--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n-bar\n' patch = '--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n-bar\n'

View File

@ -61,7 +61,7 @@ class TestSortFilesByMainLanguages:
type('', (object,), {'filename': 'file1.py'})(), type('', (object,), {'filename': 'file1.py'})(),
type('', (object,), {'filename': 'file2.java'})() type('', (object,), {'filename': 'file2.java'})()
] ]
expected_output = [{'language': 'Other', 'files': []}] expected_output = [{'language': 'Other', 'files': files}]
assert sort_files_by_main_languages(languages, files) == expected_output assert sort_files_by_main_languages(languages, files) == expected_output
# Tests that function handles empty files list # Tests that function handles empty files list

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

View File

@ -0,0 +1,31 @@
# Generated by CodiumAI
from pr_agent.algo.utils import try_fix_yaml
import pytest
class TestTryFixYaml:
# The function successfully parses a valid YAML string.
def test_valid_yaml(self):
review_text = "key: value\n"
expected_output = {"key": "value"}
assert try_fix_yaml(review_text) == expected_output
# The function adds '|-' to 'relevant line:' if it is not already present and successfully parses the YAML string.
def test_add_relevant_line(self):
review_text = "relevant line: value: 3\n"
expected_output = {"relevant line": "value: 3"}
assert try_fix_yaml(review_text) == expected_output
# The function removes the last line(s) of the YAML string and successfully parses the YAML string.
def test_remove_last_line(self):
review_text = "key: value\nextra invalid line\n"
expected_output = {"key": "value"}
assert try_fix_yaml(review_text) == expected_output
# The YAML string is empty.
def test_empty_yaml_fixed(self):
review_text = ""
assert try_fix_yaml(review_text) is None