Compare commits

..

668 Commits
v0.11 ... v0.2

Author SHA1 Message Date
c1c89577f7 Merge remote-tracking branch 'origin/main' 2024-03-11 11:58:44 +02:00
d83643083f fixed numbers 2024-03-11 11:58:35 +02:00
42cd85b420 Merge pull request #769 from Codium-ai/hl/incremental_review_update
fix
2024-03-11 11:33:48 +02:00
476d64bc18 fix 2024-03-11 11:30:05 +02:00
72cbf36e6c Merge pull request #768 from Codium-ai/hl/incremental_review_update
Hl/incremental review handle edge cases
2024-03-11 10:49:29 +02:00
ad240f7640 small fix 2024-03-11 10:13:41 +02:00
5c20fffee9 fix when no previous review where found, reivew -i should run as regular review 2024-03-11 09:59:16 +02:00
84dadb0469 rename + check github 2024-03-11 09:42:10 +02:00
b4776d5e00 Merge remote-tracking branch 'origin/main' 2024-03-11 09:38:01 +02:00
86dc87831b remove is_temporary 2024-03-11 09:37:51 +02:00
Tal
351e1b94a8 Merge pull request #767 from Codium-ai/tr/readme2
readme
2024-03-11 00:15:23 -07:00
4739fd3d37 readme 2024-03-11 09:13:35 +02:00
Tal
75800a745c Merge pull request #764 from Codium-ai/hl/incremental_review_update
Hl/incremental review update
2024-03-11 00:03:16 -07:00
Tal
256d1a3369 Merge pull request #751 from danstis/fix/ado-spaces
Fix URL encoding in Azure DevOps webhook handler
2024-03-10 23:57:59 -07:00
Tal
0efa4c1319 Merge pull request #765 from Codium-ai/tr/fixes5
Enhance logging and refactor code in gitlab_webhook.py
2024-03-10 23:57:33 -07:00
33a911da4c log 2024-03-11 08:56:48 +02:00
a27ad0feb6 log 2024-03-11 08:55:07 +02:00
6c5697b07c Enhance logging and refactor code in gitlab_webhook.py, update logger level in bitbucket_app.py and github_polling.py, remove redundant section in github.md 2024-03-11 08:50:19 +02:00
5453507097 small fix 2024-03-10 17:19:27 +02:00
003c49052e skip incremental review if no file changed since last review 2024-03-10 17:15:25 +02:00
Tal
5f529c9adf Update index.md 2024-03-10 16:16:25 +02:00
Tal
fd1fdc90dd Merge pull request #763 from Codium-ai/tr/readme_links
Update README.md with relevant links
2024-03-10 07:04:49 -07:00
6d52bcc636 Update README.md with relevant links 2024-03-10 16:02:24 +02:00
ff2346eacc update markdown 2024-03-10 14:18:29 +02:00
Tal
1cf6d1dfe1 Merge pull request #762 from Codium-ai/hl/add_manual_similar_docs
Update similar_code.md
2024-03-10 04:04:37 -07:00
1dc933b123 Update similar_code.md 2024-03-10 12:58:52 +02:00
Tal
6181f5290f Update README.md 2024-03-10 12:35:53 +02:00
Tal
fc400147d9 Merge pull request #761 from Codium-ai/tr/readme_links
Update README.md with new documentation portal links
2024-03-10 03:35:12 -07:00
498334ac5d Update README.md with relevant links 2024-03-10 12:34:17 +02:00
c8f93b1d47 Update README.md with new documentation portal links 2024-03-10 12:24:55 +02:00
Tal
1597b888e3 Merge pull request #760 from Codium-ai/tr/readme_links
Tr/readme links
2024-03-10 03:23:15 -07:00
212e7f6fb9 Update README.md with new documentation portal links 2024-03-10 12:22:30 +02:00
869978b977 Update README.md with new documentation portal links 2024-03-10 12:19:28 +02:00
Tal
a7e6690771 Merge pull request #757 from Codium-ai/add-docs-portal
Add docs portal
2024-03-10 01:45:37 -08:00
Tal
d1a2f596c7 Merge pull request #759 from Codium-ai/tr/fix_prompts
fixed bug in inline_code_suggestions in review
2024-03-09 21:53:26 -08:00
b4e9c55d8d fixed bug in inline_code_suggestions in review 2024-03-10 07:50:12 +02:00
Tal
def78ea723 Merge pull request #758 from Codium-ai/tr/fix_prompts
Fix missing colons in class definitions in pr_reviewer_prompts.toml
2024-03-09 21:40:49 -08:00
bb341a6568 Fix missing colons in class definitions in pr_reviewer_prompts.toml 2024-03-10 07:39:10 +02:00
2943204b9b Create CNAME 2024-03-09 21:07:49 +02:00
1be2039caa fix broken links 2024-03-09 21:01:38 +02:00
e4a26d2a0a fix broken links 2024-03-09 20:47:07 +02:00
4c0a98f865 Added local icon + logo 2024-03-09 20:29:25 +02:00
53dbd6f0b2 add favicon 2024-03-09 19:56:50 +02:00
9ff6c4ad86 Merge branch 'main' into add-docs-portal 2024-03-09 12:23:46 +02:00
c1d48c42e6 Updated logo reference in mkdocs.yml 2024-03-09 12:15:43 +02:00
73624148a5 Fix URL encoding in Azure DevOps webhook handler 2024-03-08 04:39:57 +00:00
c7315c53fc Merge pull request #748 from Codium-ai/ok/refactor_update_changelog
Refactor update changelog
2024-03-07 13:07:36 +02:00
05b0c4ff70 add cache to gitignore 2024-03-07 11:14:24 +02:00
228cabf610 remove cache 2024-03-07 11:13:30 +02:00
Tal
51b4cd05c3 Update CI_FEEDBACK.md 2024-03-07 10:01:40 +02:00
1ff0afabae Refactor update changelog 2024-03-07 00:48:33 +02:00
ff2b4864b4 Update README.md 2024-03-06 22:25:21 +02:00
992865dc70 Update docs-ci.yaml 2024-03-06 22:24:55 +02:00
afef57bb87 Update README.md 2024-03-06 22:22:37 +02:00
f9bef46d76 Update docs-ci.yaml 2024-03-06 22:22:11 +02:00
a602359f24 Add GitHub Actions workflow for docs deployment and remove outdated markdown files 2024-03-06 22:20:20 +02:00
852359e6ea Add documentation portal with MKDocs 2024-03-06 21:52:11 +02:00
fbbd65873e Merge pull request #747 from Codium-ai/tr/claude3
Refactor litellm_ai_handler.py and update requirements.txt
2024-03-06 12:31:18 +02:00
0690f2bbfd Refactor litellm_ai_handler.py and update requirements.txt
- Replace retry library with tenacity for better exception handling
- Add verbosity level checks for logging prompts and AI responses
- Add support for HuggingFace API base and repetition penalty in chat completion
- Update requirements.txt with tenacity library
2024-03-06 12:13:54 +02:00
Tal
8083a6e74a Merge pull request #746 from Codium-ai/tr/claude3
Tr/claude3
2024-03-06 01:08:41 -08:00
26fb2a4164 Add support for Anthropic Claude-3 model in configuration and update Usage.md 2024-03-06 08:20:08 +02:00
1c856a7d41 upgrade litellm 2024-03-06 08:06:59 +02:00
2f6f1d5879 Add command execution functionality to Bitbucket app and update configuration settings 2024-03-06 07:53:13 +02:00
Tal
3016d5d1ed Update REVIEW.md 2024-03-05 20:51:17 +02:00
Tal
f777b6e765 Merge pull request #744 from Codium-ai/tr/final_update_message
Add 'final_update_message' option to control publishing of update messages
2024-03-05 10:12:23 -08:00
c7eb70d00d protections 2024-03-05 20:11:39 +02:00
d77db93f80 protections 2024-03-05 18:34:18 +02:00
359a15c84e Add 'final_update_message' option to control publishing of update message in persistent comments 2024-03-05 17:33:29 +02:00
acefbff62b Add 'final_update_message' option to control publishing of update message in persistent comments 2024-03-05 17:29:17 +02:00
Tal
7e993558e0 Merge pull request #735 from Codium-ai/tr/fixes3
Add unique_strings function and remove duplicate issues in utils.py
2024-03-04 07:37:13 -08:00
a1355fa12a Merge pull request #736 from Codium-ai/ok/azure_openai_doc_fix
Doc update
2024-03-04 17:04:11 +02:00
273f361e31 Update section title in Usage.md from 'Usage Types' to 'Automation and Usage' 2024-03-04 15:28:21 +02:00
96bccb3156 Doc update 2024-03-04 14:28:32 +02:00
da39149c61 Add unique_strings function and remove duplicate issues in utils.py; Update pr_reviewer_prompts.toml template 2024-03-04 11:07:39 +02:00
Tal
234a4f352d Merge pull request #734 from Codium-ai/tr/fixes3
Update suggestion content in pr_code_suggestions_prompts.toml
2024-03-03 22:18:57 -08:00
eed23a7aaa Add truncation and summarization features to PR code suggestions 2024-03-04 08:16:05 +02:00
248c6b13be Update suggestion content in pr_code_suggestions_prompts.toml 2024-03-04 07:56:44 +02:00
Tal
7e07347b99 Merge pull request #733 from Codium-ai/tr/fixes3
readme updates
2024-03-03 05:10:08 -08:00
aa9dbf7111 updated readme 2024-03-03 15:04:59 +02:00
Tal
0709d5f663 Merge pull request #730 from Codium-ai/tr/issue_header
Enhance markdown formatting fo issues
2024-03-03 04:06:04 -08:00
c372c71514 prompt 2024-03-03 14:04:09 +02:00
b3fd05c465 try-except 2024-03-03 13:58:10 +02:00
Tal
68fab0bab9 Merge pull request #732 from Codium-ai/tr/remove_outdated_option
Remove functionality and references to 'remove_previous_review_comment'
2024-03-03 01:27:16 -08:00
f1bd67b7e9 Remove functionality and references to 'remove_previous_review_comment' option 2024-03-03 11:24:30 +02:00
Tal
5500d35856 Update Usage.md 2024-03-03 08:21:04 +02:00
5880221d00 Update logger setup to include debug level in github_app.py 2024-03-02 23:32:26 +02:00
Tal
c031653f68 Update README.md 2024-03-02 22:27:33 +02:00
Tal
5949a794c2 Update README.md 2024-03-02 22:26:24 +02:00
Tal
49dda61642 Update README.md 2024-03-02 22:25:55 +02:00
Tal
488caf2a90 Update Usage.md 2024-03-02 20:46:47 +02:00
Tal
9a0288250d Merge pull request #729 from Codium-ai/tr/wiki
wiki
2024-03-02 10:24:27 -08:00
917bdd5cb8 Refactor Usage.md for improved readability and organization 2024-03-02 20:23:10 +02:00
aabe96c7ff Refactor Usage.md for improved readability and organization 2024-03-02 20:20:08 +02:00
db796416d9 wiki 2024-03-02 20:16:29 +02:00
35315c070f major 2024-03-01 13:18:53 +02:00
7d081aa1d1 fix tests 2024-03-01 13:09:47 +02:00
e589dcb489 Enhance markdown formatting and update prompt descriptions in pr_reviewer_prompts.toml 2024-03-01 13:02:50 +02:00
Tal
60bd57538a Merge pull request #728 from Codium-ai/mrT23-patch-5
Update IMPROVE.md
2024-02-29 07:22:43 -08:00
Tal
a6cdd72966 Update IMPROVE.md 2024-02-29 17:20:15 +02:00
a86a3f52f0 a minor bugfix 2024-02-29 16:03:00 +02:00
Tal
2340f95488 Merge pull request #727 from Codium-ai/tr/repo_log
log event
2024-02-28 22:10:26 -08:00
dd4dc4b761 log event 2024-02-29 08:09:25 +02:00
Tal
6a51a646ee Merge pull request #725 from Codium-ai/tr/repo_log
get_log_context
2024-02-28 11:26:53 -08:00
8d498cd70c git_org 2024-02-28 16:14:13 +02:00
d5e72c2183 get_log_context 2024-02-28 16:10:57 +02:00
b09b936b15 get_log_context 2024-02-28 16:05:52 +02:00
Tal
6f22e5f557 Merge pull request #714 from Codium-ai/tr/improve_tweaks
improve tweaks
2024-02-28 05:57:38 -08:00
dbe772e708 capitalize 2024-02-28 13:59:04 +02:00
9c7ac125e1 Tweak analytics log message to add api_url 2024-02-28 12:05:01 +02:00
a8c5ac10b6 Merge remote-tracking branch 'origin/main' into tr/improve_tweaks 2024-02-28 11:09:48 +02:00
Tal
d54c5574a1 Merge pull request #723 from Codium-ai/tr/fixes2
some fixes
2024-02-27 23:27:54 -08:00
047c370683 Update README.md and add gfm markdown support check in pr_help_message.py 2024-02-28 09:20:14 +02:00
07f507c442 remove_initial_comment 2024-02-28 09:08:48 +02:00
39538c5356 cleaning 2024-02-28 09:01:39 +02:00
0c654b3b64 handle unsupported platforms for update changelog 2024-02-28 08:41:25 +02:00
Tal
92d3627de0 Update IMPROVE.md 2024-02-27 23:10:44 +02:00
4316d00941 log_context 2024-02-26 21:12:28 +02:00
edc9d8944e Refactor handle_closed_pr function to check for merged PRs 2024-02-26 20:56:43 +02:00
910c56c851 Merge pull request #721 from Codium-ai/ok/identity_provider
Identity provider
2024-02-26 20:23:09 +02:00
ab29cf2b30 Identity provider 2024-02-26 20:21:20 +02:00
Tal
540433b82c Merge pull request #720 from Codium-ai/tr/pr_statistics
Add functionality to calculate and log PR statistics on closure
2024-02-26 10:10:07 -08:00
60a37158b1 Add functionality to calculate and log PR statistics on closure 2024-02-26 20:09:01 +02:00
4921c26432 Add functionality to calculate and log PR statistics on closure 2024-02-26 20:02:11 +02:00
Tal
34fe2721fb Merge pull request #719 from Codium-ai/ok/identity_provider
Ok/identity provider
2024-02-26 09:34:06 -08:00
8bdc90c0f7 Identity provider 2024-02-26 19:00:21 +02:00
77831c793d Identity provider 2024-02-26 18:31:12 +02:00
Tal
5a2b5d97a0 Merge pull request #717 from Codium-ai/tr/logs_message
Add documentation for 'ignore_pr_titles' parameter in Usage.md
2024-02-26 06:43:22 -08:00
86c7c495f2 Add documentation for 'ignore_pr_titles' parameter in Usage.md 2024-02-26 16:29:40 +02:00
4b8fe84f88 Merge pull request #716 from Codium-ai/ok/analytics_support
Add analytics logging
2024-02-26 16:25:32 +02:00
8843f7bc8b Add analytics logging 2024-02-26 16:15:23 +02:00
Tal
64feef585a Merge pull request #715 from Codium-ai/tr/logs_message
small log improvement
2024-02-26 05:19:49 -08:00
ffe4512b7d small log improvement 2024-02-26 15:16:59 +02:00
a5cb35418e remove 'review_requested' (can trigger rate limit, and doesnt make sense algorighmically) 2024-02-26 14:20:50 +02:00
8594c93186 improve tweaks 2024-02-26 12:05:29 +02:00
Tal
116dd75a14 Merge pull request #712 from Codium-ai/tr/review_tweaks
Tr/review tweaks
2024-02-26 01:19:36 -08:00
85cdf05ca8 review formatting 2024-02-26 09:36:16 +02:00
7c9a389abf review formatting 2024-02-26 09:27:13 +02:00
18472492bc s 2024-02-26 09:14:12 +02:00
Tal
9002dccf6b Update IMPROVE.md 2024-02-25 19:10:56 +02:00
Tal
3ad53a34cd Update REVIEW.md 2024-02-25 17:33:24 +02:00
Tal
29714f9bd7 Merge pull request #710 from Codium-ai/tr/direct_link_bug
Refactor link generation in github_provider.py to use get_pr_url method
2024-02-25 07:28:47 -08:00
Tal
f921b5e9b9 Merge pull request #711 from Codium-ai/mrT23-patch-4
Update REVIEW.md
2024-02-25 07:23:04 -08:00
5f9969f30c base_url 2024-02-25 17:22:35 +02:00
Tal
0430f68d39 Update REVIEW.md 2024-02-25 17:17:55 +02:00
Tal
7f7045fd8a Update REVIEW.md 2024-02-25 17:16:10 +02:00
2dfddd8cea Fix a bug 2024-02-25 17:13:35 +02:00
bc88e0492f Fix a bug 2024-02-25 17:12:40 +02:00
a15d4f7a94 base_url 2024-02-25 16:55:20 +02:00
4258ce165b Refactor link generation in github_provider.py to use get_pr_url method 2024-02-25 16:33:19 +02:00
Tal
39e2b02933 Merge pull request #709 from Codium-ai/tr/adjustment
adjustment
2024-02-25 06:27:37 -08:00
1275cf0123 adjustment 2024-02-25 16:23:44 +02:00
5ab69af5a7 Merge pull request #695 from Codium-ai/ok/analytics
Add support for analytics file
2024-02-25 11:17:09 +02:00
Tal
118c9addaa Merge pull request #708 from Codium-ai/tr/logs
Refactor logging
2024-02-25 00:47:49 -08:00
dad3d3429f artifact 2024-02-25 10:45:15 +02:00
984a2888ae Refactor logging statements for better readability and debugging 2024-02-25 10:04:04 +02:00
8252b98bf5 Refactor logging statements for better readability and debugging 2024-02-25 10:01:53 +02:00
34e421f79b Refactor logging statements for better readability and debugging 2024-02-25 09:58:58 +02:00
877796b539 Refactor logging statements for better readability and debugging 2024-02-25 09:46:07 +02:00
Tal
df3a463668 Merge pull request #707 from KennyDizi/main
Introduced collapsible file list threshold as a constant
2024-02-24 06:29:44 -08:00
3bcf085f61 Fix context 2024-02-24 17:00:58 +07:00
f3a712683a Use constant in stead of hard code number for collapsible file list threadhold 2024-02-24 16:53:18 +07:00
Tal
51ce484bab Update github_app.py 2024-02-24 09:40:15 +02:00
Tal
214f65902c Merge pull request #701 from Codium-ai/ok/github_app_startup
Change github app startup logic to support gunicorj
2024-02-23 11:14:28 -08:00
4d8c38e5e1 Change github app startup logic to support gunicorj 2024-02-23 18:29:35 +02:00
2242f73661 Merge remote-tracking branch 'origin/ok/github_app_startup' into ok/github_app_startup 2024-02-23 18:29:09 +02:00
2f3171e422 Change github app startup logic to support gunicorj 2024-02-23 18:28:58 +02:00
90599f53d4 Change github app startup logic to support gunicorj 2024-02-23 18:27:34 +02:00
b878f64793 add analytics support 2024-02-23 17:30:20 +02:00
4242e157ab Merge remote-tracking branch 'origin/main' into ok/analytics 2024-02-23 17:16:43 +02:00
Tal
6df2a6769e Update DESCRIBE.md 2024-02-22 22:03:39 +02:00
Tal
dadb760aae Merge pull request #696 from Codium-ai/tr/app_refactor
Refactor GitHup App
2024-02-22 09:28:44 -08:00
85492f20fa 'debug' for request body 2024-02-22 18:13:42 +02:00
8b76eb1014 fixed bugs with incremental review 2024-02-22 18:03:00 +02:00
adc5709b29 Refactor github_app.py to improve handling of PR events and comments 2024-02-22 17:34:51 +02:00
b884920ef2 Refactor github_app.py to improve handling of PR events and comments 2024-02-22 17:26:47 +02:00
4ebac16ff7 Add support for analytics file 2024-02-22 13:21:56 +02:00
ee59d34e39 Merge pull request #694 from Codium-ai/ok/title_regex
Adds an option to ignore PR opens by regex matching
2024-02-22 12:26:57 +02:00
e3dba12fea Adds an option to ignore PR opens by regex matching 2024-02-22 12:14:04 +02:00
Tal
42bcda1eb8 Merge pull request #687 from Codium-ai/tr/ado
Update README.md to include Azure DevOps in supported platforms
2024-02-21 11:10:51 -08:00
1b214114ec Update README.md to include Azure DevOps in supported platforms 2024-02-21 21:06:47 +02:00
Tal
764aa8d679 Merge pull request #686 from Codium-ai/tr/reduce_number_of_calls
Tr/reduce number of calls
2024-02-21 08:26:10 -08:00
4d0f691b64 repo_settings 2024-02-21 18:23:34 +02:00
048d90623f Ignore comment not starting with a slash 2024-02-21 17:59:58 +02:00
Tal
7c87c9d3a5 Merge pull request #685 from Codium-ai/tr/reduce_number_of_calls
repo_settings
2024-02-21 07:20:21 -08:00
8ffdaf00c1 repo_settings 2024-02-21 17:18:54 +02:00
Tal
a94b5c6682 Merge pull request #684 from Codium-ai/tr/reduce_number_of_calls
Tr/reduce number of calls
2024-02-21 07:05:00 -08:00
fc7b267c9a self.diff_files 2024-02-21 17:00:11 +02:00
e291bd352e protections 2024-02-21 16:46:57 +02:00
f08ce53de3 Optimize PR commit retrieval and caching in GitHub provider and utils 2024-02-21 16:33:32 +02:00
Tal
34797fe809 Merge pull request #683 from Codium-ai/tr/fixes
Tr/fixes
2024-02-21 01:11:40 -08:00
f3c1c61c2e readme 2024-02-21 11:11:03 +02:00
0f1614bedc readme 2024-02-21 11:10:03 +02:00
a41a427b58 readme 2024-02-21 11:08:08 +02:00
b1dfd905c4 text 2024-02-21 09:40:39 +02:00
dd5386e07e try-except 2024-02-21 09:27:40 +02:00
275f0d6a05 Update GitLab configuration and documentation for webhook setup 2024-02-21 09:20:28 +02:00
0e3417b4ab webhook 2024-02-21 08:55:59 +02:00
Tal
d0a86ab684 Merge pull request #682 from jfouchard/patch-2
Add a note about change the target for Gitlab install
2024-02-20 06:58:21 -08:00
1348a67cd2 Add more detail to the pr-agent url 2024-02-20 08:33:34 -05:00
7b98db20d5 Add a note about change the target for Gitlab install
This is just a minor documentation update about changing the target when building the Docker image for Gitlab.  While it's obvious in retrospect, if you jump straight to the Gitlab section of the document how this is supposed to work.  If you follow the directions exactly you run into [this issue](https://github.com/Codium-ai/pr-agent/issues/456)
2024-02-20 08:24:43 -05:00
Tal
a4467a5773 Merge pull request #679 from Codium-ai/tr/fixes
Refactor reaction handling in GitHub provider and update help text
2024-02-19 22:11:48 -08:00
4a0b12c036 Refactor reaction handling in GitHub provider and update help text in PR tools 2024-02-20 08:06:33 +02:00
Tal
6eca495801 Merge pull request #666 from yochail/yochail/support_azure_devops_managed_identity
Add Az Devops managed identity support
2024-02-19 21:44:23 -08:00
Tal
5c8160444a Merge pull request #678 from Codium-ai/tr/help_invoke
Tr/help invoke
2024-02-19 11:48:13 -08:00
82ba285395 finalize 2024-02-19 21:44:00 +02:00
2be0339a27 help improved 2024-02-19 21:42:13 +02:00
Tal
3e7f83ffdb Merge pull request #677 from Codium-ai/tr/help_invoke
help improved
2024-02-19 11:21:31 -08:00
8d6c6a35db help improved 2024-02-19 21:10:20 +02:00
34e89e45bd help improved 2024-02-19 21:07:24 +02:00
Tal
c3ff5c46a6 Merge pull request #676 from Codium-ai/tr/bitbucket_review_suggestions
bitbucket code suggestions
2024-02-19 09:55:14 -08:00
345f923569 bitbucket code suggestions 2024-02-19 19:52:49 +02:00
0f815876e5 bitbucket code suggestions 2024-02-19 19:46:57 +02:00
d47a840179 bitbucket code suggestions 2024-02-19 19:43:31 +02:00
3770704db7 reset commit 2024-02-19 08:35:45 -05:00
Tal
9cc147dda0 Merge pull request #675 from Codium-ai/hl/notification_readme
Update Usage.md
2024-02-19 04:10:34 -08:00
787b82d888 Update Usage.md 2024-02-19 14:07:46 +02:00
Tal
20483d63b7 Merge pull request #674 from Codium-ai/tr/help_command
Refactor PR help message and update related documentation
2024-02-18 22:35:32 -08:00
36aa22bd18 Refactor PR help message and update related documentation 2024-02-19 08:30:45 +02:00
Tal
d9775e6b8c Merge pull request #673 from Codium-ai/tr/help_command
Added PRHelpMessage to command execution in pr_agent.py
2024-02-18 03:16:36 -08:00
28e8707c1b Added PRHelpMessage to command execution in pr_agent.py 2024-02-18 13:16:07 +02:00
687ece1e86 Added PRHelpMessage to command execution in pr_agent.py 2024-02-18 13:09:17 +02:00
0515b80247 Added PRHelpMessage to command execution in pr_agent.py 2024-02-18 13:08:05 +02:00
ed5856493c Added PRHelpMessage to command execution in pr_agent.py 2024-02-18 13:06:57 +02:00
e9382b18b6 Added PRHelpMessage to command execution in pr_agent.py 2024-02-18 12:01:16 +02:00
Tal
a70e5c9e1f Update README.md 2024-02-18 09:57:34 +02:00
Tal
9991fde864 Merge pull request #669 from Codium-ai/tr/readme_18_2
readme
2024-02-17 22:27:51 -08:00
163132dd6d quotes 2024-02-18 08:25:30 +02:00
8b27559510 table 2024-02-18 08:22:59 +02:00
688cb374f6 lint 2024-02-18 08:19:01 +02:00
e821ba2a5b readme 2024-02-18 08:16:47 +02:00
d371bff0ba readme 2024-02-18 08:14:21 +02:00
Tal
7b15101051 Merge pull request #661 from Codium-ai/hl/ask_line
Hl/ask line
2024-02-17 22:08:55 -08:00
5918943959 readme 2024-02-18 08:07:12 +02:00
Tal
cd8a40c7a6 Merge pull request #665 from Codium-ai/tr/checks_readme
Update README and documentation with new CI Feedback tool
2024-02-17 22:04:59 -08:00
Tal
2b12042a85 Merge pull request #667 from Codium-ai/tr_ado
azure webhook
2024-02-17 22:01:57 -08:00
9e3b79b21b readme 2024-02-18 07:59:53 +02:00
c6cb0524b4 rstrip 2024-02-18 07:56:14 +02:00
481a4fe7a1 revert 2024-02-17 19:43:34 +02:00
de4af313ba azure dev ops 2024-02-17 19:40:06 +02:00
b402bd5591 revert azuredevops_provider.py change 2024-02-17 08:36:26 -05:00
cb3ebd9169 Update README and documentation with new CI Feedback tool 2024-02-16 20:40:45 +02:00
c98e736e3b added github action support 2024-02-16 14:49:01 +02:00
40fbd55da4 added github action support 2024-02-16 12:58:55 +02:00
3eef0a4ebd fix line selection, don't support line deletions 2024-02-15 22:21:58 +02:00
6712c0a7f8 remove unnecessary call 2024-02-15 21:43:25 +02:00
cfe794947d Gitlab /ask line works 2024-02-15 21:35:51 +02:00
24dd57e5b7 clean 2024-02-15 17:14:06 +02:00
8ed98c8a4f Add Documentation 2024-02-15 15:20:15 +02:00
fff52e9e26 Add ask line feature 2024-02-15 14:25:22 +02:00
4947c6b841 Merge pull request #660 from Codium-ai/ok/add_command_to_log
Add logging context to command execution in pr_agent.py
2024-02-15 12:27:25 +02:00
433b8d24b8 Add logging context to command execution in pr_agent.py 2024-02-15 12:13:56 +02:00
bd88c66717 Merge remote-tracking branch 'origin/main' 2024-02-15 08:45:25 +02:00
d2ad8b1dbd Refactor publish_persistent_comment method to include name parameter 2024-02-15 08:45:17 +02:00
1053fa84f6 rename azure_devops_server var 2024-02-13 22:27:07 -05:00
b833d63468 PR comment: change name to azure_devops_server 2024-02-13 22:25:52 -05:00
Tal
70b83bac78 Merge pull request #659 from Codium-ai/tr/bitbucket_review
no html bitbucket
2024-02-13 08:40:57 -08:00
54a989d30f no html bitbucket 2024-02-13 18:37:48 +02:00
480a890741 no html bitbucket 2024-02-13 18:33:22 +02:00
2f327c26e8 auto approve 2024-02-13 11:21:59 +02:00
9ff62dce08 Add legacy url support 2024-02-12 18:40:06 -05:00
e8c2ec034d Update azuredevops_server_webhook.py
fix returned HTTP status
2024-02-12 18:38:08 -05:00
bbd0d62c85 fix auto_describe key 2024-02-11 18:10:22 -05:00
8fa058ff7f add azure devops pat to secret template config 2024-02-11 18:06:56 -05:00
34378384da add get endpoint for container status 2024-02-11 17:59:02 -05:00
95344c7083 fix basic auth 2024-02-11 17:42:06 -05:00
bc38fad4db add support for auto events 2024-02-11 17:23:56 -05:00
076d8e7187 fix PR code suggestions 2024-02-11 17:17:25 -05:00
22d0c275d7 fix PR comments 2024-02-11 17:13:59 -05:00
a168defd28 clean readme 2024-02-11 17:09:09 -05:00
b7a522ed69 add docker file 2024-02-11 17:05:44 -05:00
86d4a31eef add docs 2024-02-11 17:02:14 -05:00
9a54be5414 add webhook support 2024-02-11 16:52:49 -05:00
Tal
d0958022a0 Merge pull request #649 from rajyan/patch-2
filter events to align with pr-agent's setting
2024-02-11 10:54:42 -08:00
Tal
ec2aab805d Merge pull request #650 from yochail/yocail/support_azure_inline_comment
Support Azure Inline Comment
2024-02-11 10:54:10 -08:00
47060ddcac fix PR comments
- added line position
- added try-catch per comment
2024-02-11 12:40:36 -05:00
Tal
60d6fecd37 Merge pull request #653 from Codium-ai/hl/loading_improve
add loading comment to /improve
2024-02-11 07:28:57 -08:00
Tal
bdbb101183 Merge pull request #655 from Codium-ai/ok/disclaimer
Add AI disclaimer fields to configuration.toml
2024-02-11 07:28:47 -08:00
8a677e07a2 Fix a typo 2024-02-11 16:39:38 +02:00
3f42bb6793 Add AI disclaimer fields to configuration.toml 2024-02-11 16:22:26 +02:00
3420c6cf79 update label to https://github.com/Codium-ai/pr-agent/pull/654 2024-02-11 23:21:58 +09:00
Tal
159e2f7dd6 Merge pull request #654 from rajyan/rajyan-patch-2
add pull_request event triggers for github action
2024-02-11 06:08:48 -08:00
67fde2c17e add pull_request event triggers for github action
https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
2024-02-11 21:21:20 +09:00
0e08520c0c match pr-pro 2024-02-11 13:21:59 +02:00
6c500413f1 default behavior for bitbucket 2024-02-11 13:14:47 +02:00
73d2b1565d Implement edit comment 2024-02-11 12:31:30 +02:00
a40643bbba add return response 2024-02-11 12:20:06 +02:00
d93a24bbf7 add loading comment to /improve 2024-02-11 12:14:25 +02:00
Tal
79dac3f419 Merge pull request #651 from Codium-ai/tr/br_inside_code
insert_br_after_x_chars can already handle code
2024-02-11 01:47:12 -08:00
c75413fac5 count_chars_without_html 2024-02-11 11:37:11 +02:00
4e386153ea insert_br_after_x_chars can already handle code 2024-02-11 11:32:16 +02:00
8800bad658 revamped 2024-02-11 11:00:12 +02:00
cfb576d1ae revamped 2024-02-11 10:58:13 +02:00
7c9b65ba65 prompts 2024-02-11 08:05:09 +02:00
ba854c228b Update azuredevops_provider.py 2024-02-10 22:36:01 -05:00
d8ea2731ea add support for azure inline commnets 2024-02-11 03:27:47 +00:00
078e87139a possible_issues 2024-02-09 21:34:07 +02:00
bf11033349 possible_issues 2024-02-09 21:27:22 +02:00
01fbebfc5e relevant tests 2024-02-09 12:50:51 +02:00
Tal
b660badd09 Merge pull request #646 from Codium-ai/hl/pr_review_table
Hl/pr review table
2024-02-09 01:49:59 -08:00
796e203c01 rstrip() 2024-02-09 11:45:12 +02:00
6837e43114 help 2024-02-09 11:30:28 +02:00
555151602f rstrip() 2024-02-09 11:26:43 +02:00
f74b35fb6f Merge remote-tracking branch 'origin/main' into hl/pr_review_table 2024-02-09 11:05:13 +02:00
f8e1bd3d4c get_pr_url 2024-02-09 11:02:23 +02:00
e01025706a filter events to align with pr-agent's setting 2024-02-09 17:45:29 +09:00
5af9e8e749 fix 2024-02-08 23:53:29 +02:00
24c575737c fix2 2024-02-08 23:05:56 +02:00
4175e8c467 fix test2 2024-02-08 20:49:42 +02:00
77e7463395 fix tests 2024-02-08 20:14:25 +02:00
2e1462580f s 2024-02-08 19:02:56 +02:00
fa077dc516 formatting 2024-02-08 18:55:58 +02:00
a3f4c44632 PR Review 2024-02-08 15:25:43 +02:00
4447110118 small fix 2024-02-08 14:35:39 +02:00
c2088b7752 Merge commit 'e4f177908b620e46740b03966fda9243473d979e' into hl/pr_review_table 2024-02-08 14:26:29 +02:00
ddb89a7474 New PR Reviewer with Table view 2024-02-08 14:26:14 +02:00
Tal
e4f177908b Merge pull request #644 from Codium-ai/tr/parallel_calls
Tr/parallel calls
2024-02-07 10:29:00 -08:00
b077873c3d parallel_calls 2024-02-07 08:00:16 +02:00
a7ce2b11b4 parallel_calls 2024-02-07 08:00:01 +02:00
Tal
ef1b0ce3e3 Merge pull request #643 from Codium-ai/hl/docs_add_table_trigger
DOCS: add table to files changes action
2024-02-06 21:32:25 -08:00
Tal
ea3a34be08 Update DESCRIBE.md 2024-02-07 07:31:21 +02:00
9a26ed7c43 Update DESCRIBE.md 2024-02-06 21:19:47 +02:00
5d81f31ccc Update DESCRIBE.md 2024-02-06 21:18:56 +02:00
Tal
92d2f484d2 Merge pull request #642 from Codium-ai/tr/readme
readme cleaning
2024-02-06 08:04:55 -08:00
91e77787c0 readme cleaning 2024-02-06 18:03:14 +02:00
Tal
034c654bac Update .pr_agent.toml 2024-02-06 17:47:14 +02:00
Tal
94e2f00c06 Merge pull request #641 from Codium-ai/tr/auto_approve
auto approval feature
2024-02-06 07:18:43 -08:00
2bc398f74e readme 2024-02-06 16:48:29 +02:00
1c9bd3e9a8 get_pr_url 2024-02-06 09:26:00 +02:00
8a04a4f481 auto approval 2024-02-06 09:12:52 +02:00
3e96812c5d Merge remote-tracking branch 'origin/main' into tr/auto_approve 2024-02-06 09:10:00 +02:00
b190b1879e auto approval 2024-02-06 09:09:07 +02:00
Tal
9d3e6620c1 Merge pull request #640 from Codium-ai/tr/publish_output
publish_output fix
2024-02-05 21:47:23 -08:00
Tal
4636c8a1fa Merge pull request #634 from Codium-ai/tr/secret_provider
protections for 'get_secret_provider'
2024-02-05 21:46:17 -08:00
3773303bfd publish_output fix 2024-02-06 07:44:20 +02:00
Tal
a126ef64fc Merge pull request #639 from Codium-ai/tr/describe_bullets
Tr/describe bullets
2024-02-05 05:55:25 -08:00
c1c7b3b6da fixed code 2024-02-05 13:00:57 +02:00
2b6e8c3f09 minor change 2024-02-05 12:39:03 +02:00
Tal
d4e78118fe Merge pull request #638 from Codium-ai/tr/describe_bullets
insert_br_after_x_chars
2024-02-05 02:06:13 -08:00
cce3c70369 - patch_extra_lines = 1
- describe is with turbo model (for larger context)
2024-02-05 12:03:30 +02:00
32e8ba331a insert_br_after_x_chars 2024-02-05 10:12:47 +02:00
3f2a7869dd insert_br_after_x_chars 2024-02-05 09:22:26 +02:00
2ee329674f insert_br_after_x_chars 2024-02-05 09:20:36 +02:00
e104bd7a3f large patch protection 2024-02-04 16:27:57 +02:00
3e128869dc large patch protection 2024-02-04 16:10:53 +02:00
cf2ed9d483 Merge remote-tracking branch 'origin/main' 2024-02-04 14:25:57 +02:00
e1b0e4a40a minor prompt changes 2024-02-04 14:24:55 +02:00
Tal
1013f7586b Merge pull request #637 from Codium-ai/tr/pr-actions
pr-actions
2024-02-02 04:21:33 -08:00
023f2eb77c pr-actions 2024-02-02 14:17:45 +02:00
Tal
cb8ff2b318 Merge pull request #636 from Codium-ai/tr/model_turbo
moving the 'improve' command to turbo mode, with auto_extended=true
2024-02-01 06:57:42 -08:00
d04d8b616a moving the 'improve' command to turbo mode, with auto_extended=true 2024-02-01 09:46:04 +02:00
Tal
2816cd2c4b Update README.md 2024-02-01 09:06:01 +02:00
2112defa51 lancedb bump 2024-02-01 08:44:23 +02:00
9579be028d protections for 'get_secret_provider' 2024-02-01 08:31:11 +02:00
Tal
7168326911 Update TOOLS_GUIDE.md 2024-01-30 08:22:29 +02:00
Tal
e1ae51e7a0 Update TOOLS_GUIDE.md 2024-01-30 08:21:43 +02:00
Tal
c69962479a Merge pull request #630 from Codium-ai/tr/language
Enhancements in Patch Formatting and Code Suggestions Handling
2024-01-29 12:11:23 -08:00
15c8fe94bb feat: Improve patch formatting and handle empty data in pr_code_suggestions.py 2024-01-29 22:00:11 +02:00
0d86779799 feat: Improve patch formatting and handle empty data in pr_code_suggestions.py 2024-01-29 21:52:54 +02:00
6565556e01 feat: Add 'language' field to CodeSuggestion, FileDescription, and ReviewerPrompt models in settings files 2024-01-29 20:51:24 +02:00
Tal
6998089549 Update README.md 2024-01-29 20:21:23 +02:00
8d36e2e2f7 feat: Add new configuration options in pr_test section and update TEST.md documentation 2024-01-29 20:17:39 +02:00
Tal
93f1854c68 Merge pull request #629 from Codium-ai/tr/tests
s
2024-01-29 01:43:46 -08:00
40a7ef9132 s 2024-01-29 11:42:32 +02:00
042eab1641 s 2024-01-29 11:39:50 +02:00
567b400f97 Revert "s1"
This reverts commit 412159bba5.
2024-01-29 11:30:58 +02:00
412159bba5 s1 2024-01-29 11:28:58 +02:00
Tal
e6f548920b Merge branch 'main' into tr/tests 2024-01-29 01:23:27 -08:00
f1fe2563f4 s 2024-01-29 11:22:46 +02:00
467e2ae68e s 2024-01-29 11:19:37 +02:00
78bb54bd8f s 2024-01-29 11:11:30 +02:00
Tal
2bebfba4b6 Update README.md 2024-01-28 20:39:45 +02:00
Tal
815073e04f Merge pull request #628 from Codium-ai/tr/tests
tests readme
2024-01-28 10:32:27 -08:00
5f1722ed4a s 2024-01-28 20:30:40 +02:00
47af04d158 s 2024-01-28 20:26:58 +02:00
Tal
335654b02a Update Usage.md 2024-01-27 21:38:25 +02:00
Tal
68e17ed2be Merge pull request #627 from Codium-ai/tr/updates2
Tr/updates2
2024-01-27 11:33:33 -08:00
ecb46435b3 s 2024-01-27 21:29:19 +02:00
98ce0a7036 s 2024-01-27 21:25:43 +02:00
76f44b13f8 docs: Update GitHub app configurations section in Usage.md 2024-01-27 21:20:10 +02:00
06dede29f2 feat: Update configuration and handling of GitHub Action settings 2024-01-27 21:15:23 +02:00
Tal
dbf5ebcb8d Merge pull request #622 from eltociear/fix-filename
docs: fix file name
2024-01-25 02:09:27 -08:00
Tal
d6a45663f1 Merge pull request #624 from Codium-ai/hl/small_fixes
small fixes
2024-01-25 02:09:04 -08:00
07eaa59e78 small fixes 2024-01-25 11:07:43 +02:00
1e2d4e9830 docs: fix file name 2024-01-25 15:03:58 +09:00
Tal
cc03f7f615 Merge pull request #620 from Codium-ai/tr/updates
Configuration updates
2024-01-24 09:59:43 -08:00
Tal
040da2fbb1 Merge pull request #612 from Codium-ai/mrT23-patch-1
Update README.md
2024-01-24 09:59:25 -08:00
Tal
a83a492b22 Merge branch 'main' into mrT23-patch-1 2024-01-24 09:58:23 -08:00
e056cd5988 type 2024-01-24 19:55:33 +02:00
4077c5556d enable_review_labels_effort set to true by default 2024-01-24 19:49:43 +02:00
d8465ea9f9 removed include_improved_code 2024-01-24 19:47:30 +02:00
f4037e0dfa feat: Add LanceDB support for similar_issue tool and refactor SOC2 compliance feature name 2024-01-24 19:40:58 +02:00
9986f5307c Merge pull request #618 from Codium-ai/hl/describe_usage_guide
Enhance Documentation for "Inline File Walkthrough" Feature
2024-01-24 10:15:21 +02:00
60c0371854 highlight options 2024-01-23 18:13:08 +02:00
139bbfc67a update docs and usage guide 2024-01-23 17:58:55 +02:00
b33b8c12cd Merge pull request #616 from Codium-ai/hl/walkthrough_title_ui_improvements
update default config for inline_file_summary to false
2024-01-22 10:37:02 +02:00
968684b461 update default config for inline_file_summary to false 2024-01-22 10:25:34 +02:00
Tal
4a5cff4995 Update CUSTOM_SUGGESTIONS.md 2024-01-21 17:58:01 +02:00
599c6773f3 Merge pull request #613 from Codium-ai/hl/walkthrough_title_ui_improvements
Add changes title of files and improve table style and alignments
2024-01-21 17:52:44 +02:00
Tal
7178ddac10 Update CUSTOM_SUGGESTIONS.md 2024-01-21 17:31:33 +02:00
Tal
5dedc381a6 Merge pull request #615 from Codium-ai/mrT23-patch-2
Update README.md
2024-01-21 07:30:14 -08:00
Tal
cba14ada2c Update README.md 2024-01-21 17:29:29 +02:00
Tal
f81fe0a12d Merge pull request #614 from Codium-ai/tr/custom_suggestions
feat: Add custom suggestions tool to README.md
2024-01-21 07:19:22 -08:00
78d886459a feat: Add custom suggestions tool to README.md 2024-01-21 17:15:34 +02:00
27aafb06cb feat: Add custom suggestions tool to README.md 2024-01-21 17:10:23 +02:00
329f7fa9d6 feat: Add custom suggestions tool to README.md 2024-01-21 17:06:25 +02:00
e79919b5c6 update describe screenshot to the new describe 2024-01-21 14:09:17 +02:00
8d513e078a Add changes title of files and improve table style and alignments 2024-01-21 13:43:37 +02:00
Tal
69f7923552 Update README.md 2024-01-20 13:02:07 +02:00
Tal
2430a1a608 Merge pull request #594 from Codium-ai/tr/fallback_bad_review_comment
Enhanced Comment Verification and Fallback Mechanism for Inline Comment Publishing
2024-01-20 02:04:06 -08:00
e54388d807 s 2024-01-20 11:59:45 +02:00
d942bdb8bd s 2024-01-20 11:56:17 +02:00
Tal
84d87aa870 Merge pull request #607 from Codium-ai/tr/edge_cases
feat: Improve error handling and code readability in pr_agent tools
2024-01-18 07:09:07 -08:00
39891e4ab1 feat: Improve error handling and code readability in pr_agent tools 2024-01-18 17:01:25 +02:00
d7858efbbe Merge pull request #581 from Codium-ai/sm/azure_devops
Enhancement of AzureDevopsProvider with new functionalities and refactoring
2024-01-18 16:28:28 +02:00
Tal
b3365b8d6c Merge pull request #605 from Codium-ai/tr/edge_cases
No suggestions found
2024-01-18 06:18:43 -08:00
fc5b00f4d3 s 2024-01-18 16:11:44 +02:00
Tal
5150e66723 Merge pull request #603 from Codium-ai/mrT23-patch-1
Update README.md
2024-01-17 22:21:44 -08:00
Tal
4dad1af77b Update README.md 2024-01-18 08:20:09 +02:00
Tal
02129b40cf Merge pull request #601 from Codium-ai/hl/diffview_file_summary
Readme Inline file summary 💎
2024-01-17 06:37:53 -08:00
3fb6d17338 width 2024-01-17 16:36:26 +02:00
3be7bfce79 feat: Add repository labels retrieval function in gitlab_provider.py
docs: Update links and add Inline file summary to TOC in DESCRIBE.md
2024-01-17 16:33:48 +02:00
472646ddfd Readme 2024-01-17 16:27:07 +02:00
eb4a1c515e Merge pull request #600 from Codium-ai/tr/improve_usage_guide
readme updates
2024-01-17 15:55:42 +02:00
e4af0b22ad s 2024-01-17 15:51:42 +02:00
a3e59a418e Merge remote-tracking branch 'origin/main' into tr/improve_usage_guide 2024-01-17 15:46:05 +02:00
4e833c0c28 s 2024-01-17 15:43:01 +02:00
Tal
0b811d97a7 Merge pull request #598 from Codium-ai/tr/improve_usage_guide
Enhancements to the 'improve' tool and updates to the related documentation
2024-01-17 03:13:33 -08:00
8f510dc553 s 2024-01-17 11:47:59 +02:00
2132771f46 s 2024-01-17 11:29:50 +02:00
e66bd7caa7 fallback to commitable 2024-01-17 11:18:30 +02:00
17ce2f0ed0 improve usage guide 2024-01-17 10:09:44 +02:00
7298548f82 improve usage guide 2024-01-17 10:06:27 +02:00
298c41a100 improve usage guide 2024-01-17 10:03:48 +02:00
58163e5129 improve usage guide 2024-01-17 09:50:48 +02:00
Tal
fae3bf6309 Merge pull request #590 from EduardDurech/patch-2
Fixed Run from source instructions for Python
2024-01-16 22:53:36 -08:00
06f0235577 Merge pull request #597 from Codium-ai/hl/improve_ui_table
Hl/improve UI table
2024-01-16 09:46:34 +02:00
d7e0aad527 small fixes 2024-01-16 09:41:31 +02:00
31576b77ff improve backticks 2024-01-15 19:07:41 +02:00
ea39e8684f works 2024-01-15 16:42:50 +02:00
afefc15b9c improve doce suggestions UI with difflib 2024-01-15 15:56:48 +02:00
5e17ccaf86 add colaplsable 2024-01-15 15:17:57 +02:00
9b1eb86d75 first iteration of improved UI for /improve --extended 2024-01-15 15:10:54 +02:00
9f5c2e5f17 feat: Refactor comment verification in github_provider.py 2024-01-14 11:55:07 +02:00
7377f4e4b2 feat: Refactor comment verification in github_provider.py 2024-01-14 11:49:51 +02:00
d6f4c1638d feat: Refactor comment verification in github_provider.py 2024-01-14 10:49:05 +02:00
a58c385b0f Fixed Rust warning tip as behaviour is inconsistent 2024-01-14 04:16:32 +01:00
7a3830d228 Fixed Run from source instructions for Python
Previously only installed dependencies but not pr_agent

+ Fixed link to OpenAI API Key and added for GitHub access token
2024-01-13 06:29:07 +01:00
Tal
e7251ada3f Merge pull request #588 from barnett-yuxiang/ignore-ds-store
Ignore .DS_Store files
2024-01-10 07:25:49 -08:00
aca3fcb571 Ignore .DS_Store files
This commit updates the .gitignore file to ignore .DS_Store files, which are created by macOS. These files are not relevant to the project and should not be included in version control.

Signed-off-by: Kamakura.Yx <barnett.yuxiang@gmail.com>
2024-01-10 23:13:57 +08:00
b9951fce62 Typo when parsing the suggestion part 2024-01-10 11:59:45 +02:00
Tal
609836bd6a Merge pull request #587 from Codium-ai/tr/abbrevations
Enhancement: Improve PR-Agent Tool with Custom Labels, Extra Instructions, and Summarize Mode
2024-01-09 23:39:04 -08:00
09ee0b64ba feat: Refactor instructions and fields in pr_code_suggestions_prompts.toml 2024-01-10 09:37:05 +02:00
Tal
fb4746fd09 Merge pull request #584 from samanhappy/link
Fix link formatting for relevant_line
2024-01-09 23:22:55 -08:00
729b5d11c9 feat: Refactor instructions and fields in pr_code_suggestions_prompts.toml 2024-01-09 22:56:25 +02:00
fc502a6fd5 feat: Refactor instructions and fields in pr_code_suggestions_prompts.toml 2024-01-09 22:49:26 +02:00
2b607dbd9a feat: Refactor instructions and fields in pr_code_suggestions_prompts.toml 2024-01-09 22:32:09 +02:00
9c6aabb0bb feat: Add custom labels and extra instructions sections to help.py, summarize mode to pr_code_suggestions.py, and summarize mode condition to pr_code_suggestions_prompts.toml 2024-01-09 22:09:48 +02:00
Tal
da3ac656ee Merge pull request #586 from Codium-ai/tr/ask_usage
ask helper
2024-01-09 06:55:30 -08:00
a42e57d09b ask helper 2024-01-09 16:36:39 +02:00
e56c443fd6 ask helper 2024-01-09 16:34:27 +02:00
abc05e7711 ask helper 2024-01-09 16:25:23 +02:00
Tal
a77d539866 Merge pull request #585 from samanhappy/url
Enhancement: Update GitLab link generation to support self-managed GitLab server and different projects
2024-01-09 06:12:05 -08:00
19c14b940e Try fixing invalid inline comments 2024-01-09 09:54:29 +02:00
36f1cfb51f Enhancement: Update GitLab link generation to support self-managed GitLab server and different projects 2024-01-09 15:11:27 +08:00
0f2a4654a7 Fix link formatting for relevant_line 2024-01-09 14:56:18 +08:00
28c5ad1d8b nit 2024-01-08 13:06:03 +02:00
2bb5ae8c0d Remove redundant condition (status 422 already means the same) 2024-01-08 13:05:10 +02:00
b0bffdec84 Refactor and add configuration toggle 2024-01-08 12:00:20 +02:00
Tal
11b96b1c1a Merge pull request #583 from Codium-ai/tr/unique_titles
feat: Remove bot help text from github_polling.py
2024-01-08 01:51:43 -08:00
e0f4bc7ded feat: Remove bot help text from github_polling.py 2024-01-08 11:45:01 +02:00
Tal
62d83f6753 Merge pull request #582 from Codium-ai/tr/unique_titles
Enhance PR description headers with bold formatting and include original user description
2024-01-08 01:28:56 -08:00
e9a2a0a96f s 2024-01-08 10:37:51 +02:00
46a38473e4 Merge remote-tracking branch 'origin/main' into tr/unique_titles 2024-01-08 10:30:58 +02:00
c9e55be275 s 2024-01-08 10:30:47 +02:00
Tal
f714592dec Merge pull request #579 from Codium-ai/tr/user_description
Enhancements to Logging, Help Messages, and PR Descriptions and Reviews
2024-01-07 23:46:25 -08:00
8bb2eb48af s 2024-01-08 09:43:34 +02:00
9cfb8ce475 s 2024-01-08 09:39:19 +02:00
67cb133c52 s 2024-01-08 09:28:44 +02:00
9c054bb80f s 2024-01-08 09:18:46 +02:00
b776e5069c feat: Refactor AzureDevopsProvider class in azuredevops_provider.py
- Reorder class methods and constructor for better readability
- Add error logging for failed operations
- Implement get_pr_description_full method
- Update get_pr_description method to always return full description
- Modify _parse_pr_url method to return workspace_slug, repo_slug, and pr_number
- Make _get_azure_devops_client a static method
- Add error handling in get_pr_id method
2024-01-08 09:15:34 +02:00
c8bca487e5 feat: Implement methods in AzureDevopsProvider for publishing code suggestions, labels, and removing comments 2024-01-08 08:59:12 +02:00
Tal
78fa61eac6 Merge branch 'main' into tr/user_description 2024-01-07 22:55:18 -08:00
Tal
582cbd623f Merge pull request #580 from Codium-ai/tr/add_logs
feat: Add debug logs to git_provider and pr_description modules
2024-01-07 09:58:20 -08:00
3ea08a6cf5 feat: Add debug logs to git_provider and pr_description modules 2024-01-07 19:57:49 +02:00
3154ebbf9f feat: Add debug logs to git_provider and pr_description modules 2024-01-07 19:56:05 +02:00
Tal
8fe608884d Merge pull request #578 from Codium-ai/ok/atlassian_version
feat: Update atlassian-python-api version in requirements.txt
2024-01-07 09:17:56 -08:00
60e79f0134 feat: Update atlassian-python-api version in requirements.txt 2024-01-07 18:08:11 +02:00
a6bbd04efb s 2024-01-07 17:02:18 +02:00
578d15c6fc usage guide 2024-01-07 16:38:08 +02:00
22d17985a1 Less noisy fallback for publish_code_suggestions in case of invalid comments
As a first option, `publish_code_suggestions` will try to post all review comments in a single GitHub review. This is preferred because it will group all comments together in the GitHub UI under the same review, and will trigger just one notification for any viewers of the PR.

If just one of the comments is malformed, the entire API request will fail and none of the comments will be posted to the PR. In the current implementation, the fallback mechanism is to just post each comment separately with `try/except` and skip the invalid comments. This works, but potentially creates a lot of noise in the PR as each comment is posted as in a separate review, creating multiple notifications.

This suggested fallback is based on a similar idea, but without creating multiple review notifications. The it works is by iterating over the potential comments, and starting a PENDING review for the current comment. The review is not submitted and does not trigger a notification, but it is verified against the GitHub API, and so we can verify if the comment is valid. After checking all comments we then submit a single review with all the verified comments which is guaranteed to succeed.

The end result is having the exact same comments posted to the PR as with the current fallback method, but the downside is having twice as many API calls (for each comment we have 1 extra API call to delete the pending review).
2024-01-07 16:00:44 +02:00
Tal
faba5a224a Merge pull request #575 from Codium-ai/tr/user_description
docs: Enhance 'improve' tool documentation and update 'DESCRIBE.md' w…
2024-01-07 01:30:44 -08:00
19f85f08b0 docs: Enhance 'improve' tool documentation and update 'DESCRIBE.md' with hyperlink 2024-01-07 11:29:07 +02:00
8512e9dffb docs: Enhance 'improve' tool documentation and update 'DESCRIBE.md' with hyperlink 2024-01-07 11:26:24 +02:00
7c0be8ca44 docs: Enhance 'improve' tool documentation and update 'DESCRIBE.md' with hyperlink 2024-01-07 11:22:53 +02:00
d9872d7031 docs: Enhance 'improve' tool documentation and update 'DESCRIBE.md' with hyperlink 2024-01-07 11:19:47 +02:00
Tal
be4b9c4991 Merge pull request #574 from Codium-ai/tr/user_description
Enhance and reorganize documentation for review and describe tools
2024-01-07 00:01:30 -08:00
94172104f0 docs: Reorganize and enhance documentation for review and describe tools 2024-01-07 09:56:09 +02:00
8ec790d4f7 Merge remote-tracking branch 'origin/main' into tr/user_description
# Conflicts:
#	docs/DESCRIBE.md
2024-01-07 09:49:43 +02:00
640972b00a docs: Reorganize and enhance documentation for review and describe tools 2024-01-07 09:48:17 +02:00
7810ba7d76 docs: Reorganize and enhance documentation for review and describe tools 2024-01-07 09:40:24 +02:00
d91f1fce09 docs: Reorganize and enhance documentation for review and describe tools 2024-01-07 09:36:03 +02:00
edcb666fbc docs: Reorganize and enhance documentation for review and describe tools 2024-01-07 09:28:06 +02:00
Tal
6d359fe1d8 Update DESCRIBE.md 2024-01-06 21:46:11 +02:00
Tal
19e38595f3 Merge pull request #573 from Codium-ai/tr/user_description
feat: Update adaptive collapsible file list logic in pr_description.py
2024-01-06 11:08:55 -08:00
355ef8c476 feat: Update adaptive collapsible file list logic in pr_description.py 2024-01-06 10:36:36 +02:00
Tal
f08d225360 Merge pull request #572 from Codium-ai/tr/user_description
describe docs
2024-01-06 00:15:36 -08:00
22b7dd9f2d s 2024-01-06 10:15:04 +02:00
a85a328791 s 2024-01-06 10:08:29 +02:00
82d10f24f0 s 2024-01-06 10:04:48 +02:00
Tal
c9717f1d73 Update README.md 2024-01-05 23:29:59 +02:00
Tal
82b58d5b09 Update README.md 2024-01-05 21:37:45 +02:00
Tal
b6d41d6a91 Merge pull request #571 from Codium-ai/tr/user_description
Enhance Documentation and Configuration of 'describe' Tool
2024-01-05 11:30:41 -08:00
ac74fa8431 docs 2024-01-05 21:28:54 +02:00
42704d5781 Merge remote-tracking branch 'origin/main' into tr/user_description 2024-01-05 17:03:30 +02:00
3628786a61 feat: Clarify PRType label member usage in pr_description_prompts.toml 2024-01-05 17:03:14 +02:00
Tal
c69ede0138 Update REVIEW.md 2024-01-05 11:10:55 +02:00
Tal
4349156e97 Merge pull request #570 from wesvetter/patch-1
📚 Minor update to REVIEW.md for inclusive language
2024-01-04 22:48:36 -08:00
9f88105f72 📚 Minor update to REVIEW.md for inclusive language
Replaces "his [judgement]" with "their".
2024-01-04 10:32:53 -08:00
Tal
fe6b2065fb Merge pull request #569 from zmeir/zmeir/enhance/auto_improve_extended_simple
Add toggle to automatically enable `/improve --extended`
2024-01-04 09:06:08 -08:00
c2b0891c0b Simpler auto-extended toggle and keep the default as false 2024-01-04 18:53:45 +02:00
Tal
782f1ca1bd Merge pull request #568 from Codium-ai/tr/user_description
Enhancement: Improved Extraction and Placement of User Descriptions in PRs
2024-01-04 08:35:34 -08:00
6d18a0c843 feat: Improve user description extraction in git_provider.py 2024-01-04 18:34:00 +02:00
Tal
e6093cd768 Merge pull request #567 from zmeir/zmeir/enhance/support_azure_in_langchain_ai_handler
Add support for Azure OpenAI in LangChainOpenAIHandler
2024-01-04 08:22:01 -08:00
Tal
aea0c4d45f Merge pull request #566 from zmeir/zmeir/fix/config_command_missing_arg
Fix failing `/config` command
2024-01-04 08:14:29 -08:00
1c2bb2ef3d feat: Update user description extraction and placement in PR description 2024-01-04 18:05:28 +02:00
7762bf59bf feat: Update user description extraction and placement in PR description 2024-01-04 18:01:55 +02:00
3e29848cd0 Merge remote-tracking branch 'origin/main' into tr/user_description
# Conflicts:
#	pr_agent/git_providers/git_provider.py
2024-01-04 17:49:10 +02:00
c3b5aaf8cc feat: Update user description extraction and placement in PR description 2024-01-04 17:46:24 +02:00
ba3f22d81e Move logging to a central location for all AI Handlers 2024-01-04 16:22:22 +02:00
ac7aaa0cd3 Add support for Azure OpenAI in LangChainOpenAIHandler 2024-01-04 16:22:22 +02:00
1ade09eaa3 Fix failing /config command
All commands need the `ai_handler` argument. The PRConfig class was missing it in the `__init__` method and so it failed with this error:

```
File "/home/vcap/app/pr_agent/agent/pr_agent.py", line 76, in handle_request
    await command2class[action](pr_url, ai_handler=self.ai_handler, args=args).run()
TypeError: PRConfig.__init__() got an unexpected keyword argument 'ai_handler'
```
2024-01-04 14:49:34 +02:00
Tal
b7af45166a Merge pull request #561 from zmeir/zmeir/fix/get_user_description
Fix `get_user_description`
2024-01-04 00:40:08 -08:00
Tal
92f89e6ca0 Merge pull request #565 from Codium-ai/tr/remove_old_walkthrough
Remove old 'enable_file_walkthrough' mode
2024-01-04 00:36:05 -08:00
ed78bfd946 use_collapsible_file_list 2024-01-04 10:27:07 +02:00
4204d78d7e feat: Remove file walkthrough feature from PR agent 2024-01-04 09:59:44 +02:00
3c2ed8bbf1 feat: Remove file walkthrough feature from PR agent 2024-01-04 09:42:15 +02:00
8d2da74380 Find user description in a case-insensitive way 2024-01-04 09:41:55 +02:00
39c1866121 Revert title() to capitalize() 2024-01-04 09:41:24 +02:00
Tal
1bba0162f8 Merge pull request #563 from zmeir/zmeir/enhance/refine_add_docs_prompt
Refine the prompt for `add_docs` command
2024-01-03 23:14:27 -08:00
Tal
c07ea5ea32 Merge pull request #560 from zmeir/zmeir/fix/drop_python3.9
Drop support for python 3.9
2024-01-03 23:09:54 -08:00
2f9fbbf0ac Prevent reducing the number of suggestions if already low enough 2024-01-03 16:43:39 +02:00
0189e12fb1 Automatically enable improve extended mode for large PRs 2024-01-03 16:43:38 +02:00
58f93e0615 Drop support for python 3.9
The `bitbucket_server_provider.py` uses structural pattern matching that was introduced in python 3.10, and so trying to run any command with python 3.9 will fail (because we import all the providers right at the top of `pr_agent.git_providers`)
2024-01-03 12:30:09 +02:00
967494ce17 Refine the prompt for add_docs command
I found that without it, python docstrings are sometimes suggested above the function signature, instead of below.
2024-01-03 12:27:23 +02:00
560d30dbb1 Fix get_user_description
The headers changed from "PR Type"/"PR Description"/etc to "Type"/"Description"/etc
2024-01-03 12:20:51 +02:00
Tal
c31ce3de35 Merge pull request #559 from pzarfos/fix/prompt-spelling
feat: Fix typo
2024-01-02 21:41:34 -08:00
0bd2f045a3 feat: Fix typo 2024-01-02 08:11:31 -05:00
Tal
0b4a98b3aa Merge pull request #558 from Codium-ai/tr/soc2_ticket
feat: Add SOC2 compliance review feature to PR agent 💎
2024-01-01 22:02:22 -08:00
7dfc306e7c feat: Add SOC2 compliance review feature to PR agent 2024-01-01 20:15:36 +02:00
Tal
be88624e2a Merge pull request #556 from Codium-ai/mrT23-patch-1
Update Usage.md
2023-12-31 07:35:40 -08:00
Tal
ac9a46d4c4 Update Usage.md 2023-12-31 17:34:13 +02:00
3e1349ed1f Merge pull request #554 from xens/patch-1
fix: missing flag in INSTALL.md
2023-12-28 10:29:14 -08:00
0d89e6e760 fix: missing flag in INSTALL.md
Fixing a missing flag on the Docker cli to declare a variable.
2023-12-28 17:24:08 +01:00
a9c8fb6a73 Merge pull request #552 from KennyDizi/main
Add `enable_help_text` setting and update PR review preparation method
2023-12-26 21:45:21 -08:00
fce52a66ff feat: Update enable_help_text flag explaination 2023-12-27 10:22:43 +07:00
dff31ff8f5 feat: Fix typo 2023-12-27 10:17:56 +07:00
37b040b50a Use 'and' in lieu of '&' operator 2023-12-27 10:17:08 +07:00
31168cd7de Get PR review enable help text from setting 2023-12-27 10:12:41 +07:00
db6ca434ac Update Usage.md 2023-12-26 17:15:48 +02:00
958bfe1000 Merge pull request #551 from Codium-ai/tr/global_configuration
global configuration
2023-12-26 07:08:52 -08:00
815862e428 markdown 2023-12-26 17:06:29 +02:00
b1ce29e27a global configuration 2023-12-26 16:45:39 +02:00
f7c2b3128f Merge pull request #550 from Codium-ai/tr/gfm_markdown
feat: Refactor help text addition in pr_reviewer.py and update tool n…
2023-12-25 23:30:36 -08:00
a6764c9058 feat: Update help text addition condition in pr_reviewer.py 2023-12-26 09:25:15 +02:00
a854e1a408 feat: Refactor help text addition in pr_reviewer.py and update tool names in README.md 2023-12-26 09:18:38 +02:00
ba3a8b24f0 Merge pull request #548 from PrashantDixit0/main
LanceDB Integration
2023-12-25 06:39:11 -08:00
26cb85c4f5 default pinecone 2023-12-25 14:50:15 +05:30
1d435ef3fa removed comments 2023-12-25 00:45:24 +05:30
1632696c2f Merge branch 'main' of github.com:PrashantDixit0/pr-agent 2023-12-25 00:41:28 +05:30
d8d954bb0f lancedb integration 2023-12-25 00:38:24 +05:30
08e9a91021 Merge pull request #547 from Codium-ai/tr/readme_update
Readme for PR-Agent-Pro
2023-12-24 06:30:04 -08:00
648c22ed1e tools update 2023-12-24 16:22:45 +02:00
49592ba2d7 docs: Refine markdown formatting in Analyze.md and README.md 2023-12-24 16:21:41 +02:00
0c4d451d9a readme 2023-12-24 16:18:20 +02:00
e698c7e2f3 Merge pull request #546 from Codium-ai/tr/backticks_review
Single-label for suggestions
2023-12-24 03:35:49 -08:00
663632e2d9 fixed bug 2023-12-24 10:27:48 +02:00
5fd3fdfae1 feat: Add loop to populate suggestion_list in pr_code_suggestions.py 2023-12-24 10:08:36 +02:00
47b267a73d prompt 2023-12-24 09:52:59 +02:00
5c49ff216a feat: Update inline comment creation in git providers and improve code suggestion handling
- Update `create_inline_comment` method in various git providers to include `absolute_position` parameter
- Remove `create_inline_comment` method from providers that do not support inline comments
- Enhance `find_line_number_of_relevant_line_in_file` function to handle absolute position
- Modify `pr_code_suggestions.py` to handle improved code inclusion in suggestions
- Add `include_improved_code` configuration option in `configuration.toml` and update documentation accordingly
2023-12-24 09:44:08 +02:00
5dc2595dcf feat: Refactor code suggestion handling and update YAML schema in pr_code_suggestions.py and pr_code_suggestions_prompts.toml
- Update key names in pr_code_suggestions.py to use snake_case for consistency
- Implement removal of invalid suggestions where existing code is equal to improved code
- Update YAML parsing in _prepare_pr_code_suggestions method to include keys_fix_yaml parameter
- Refactor push_inline_code_suggestions method to use updated key names
- Update _prepare_prediction_extended method to use new key names
- Refactor _prepare_markdown method to include suggestion label and use updated key names
- Update instructions and YAML schema in pr_code_suggestions_prompts.toml to reflect changes in pr_code_suggestions.py
- Remove redundant removal of invalid suggestions in rank_suggestions method
2023-12-24 08:30:35 +02:00
664b1c9d17 Merge pull request #545 from Codium-ai/tr/backticks_review
feat: Improve suggestion formatting in markdown text generation
2023-12-23 10:41:17 -08:00
ba7781ba00 feat: Update instruction formatting in pr_code_suggestions_prompts.toml and pr_reviewer_prompts.toml 2023-12-23 20:40:30 +02:00
42be96a99b feat: Improve suggestion formatting in markdown text generation 2023-12-23 20:32:03 +02:00
64a2c55d48 Merge pull request #542 from Codium-ai/tr/title_last
Enhancement: Update PR description prompts and reorder keys in PR description data
2023-12-21 03:55:17 -08:00
eca8078071 feat: Reorder keys in PR description data and update PRDescription model in toml file 2023-12-21 08:51:57 +02:00
9995ccd4c7 feat: Update PR description prompts in toml file to include semantic file types and custom labels options 2023-12-21 08:31:54 +02:00
851c001aa5 Merge pull request #541 from Codium-ai/tr/changes
feat: Enhance YAML parsing with additional fallbacks and key customiz…
2023-12-20 22:26:15 -08:00
2b23700aec feat: Enhance YAML parsing with additional fallbacks and key customization in load_yaml and try_fix_yaml functions 2023-12-21 08:24:07 +02:00
553dad0bee feat: Enhance YAML parsing with additional fallbacks and key customization in load_yaml and try_fix_yaml functions 2023-12-21 08:21:34 +02:00
37259e550f Merge pull request #540 from Codium-ai/tr/backticks
Enhancement of PR Description Formatting and Instructions Update
2023-12-20 22:16:53 -08:00
66cbd6ef8f Merge pull request #537 from koid/feature/ignore-header-description-in-ai-response
Enhancement of AI Response Parsing Mechanism
2023-12-20 22:15:43 -08:00
a9d789978b fix: remove last line 2023-12-21 11:11:46 +09:00
f99862088e re-implemented test case 2023-12-21 11:09:25 +09:00
e2797ad09a re-implemented YAML extraction as a fallback 2023-12-21 11:06:41 +09:00
ccb116922f Merge pull request #529 from lukefx/bitbucket_webhook_improvements
feat: Improved server, security and commands
2023-12-20 17:27:06 +02:00
c079deba21 feat: Enhance PR description formatting and update instructions in pr_description_prompts.toml 2023-12-20 16:45:21 +02:00
16b61eb4e8 ignore header description in ai response 2023-12-20 11:50:27 +09:00
68c26b362b Merge pull request #533 from Codium-ai/hl/fix_incomplete_yaml
bug fix
2023-12-18 10:07:07 -08:00
6e63cf4014 Add log 2023-12-18 17:35:04 +02:00
c59e9f77a6 fix 2023-12-18 17:06:02 +02:00
2a3779776a Merge pull request #532 from Codium-ai/hl/native_labels_readme
Hl/native labels readme
2023-12-18 16:12:38 +02:00
e25980f141 fix: using the same get_settings convention 2023-12-18 14:58:25 +01:00
6c80fde6df fix 2023-12-18 13:44:37 +02:00
75dcb035a7 Update 2023-12-18 13:41:50 +02:00
2ac86f429f Merge pull request #531 from Codium-ai/disable_github_action
Update pr-agent-review.yaml
2023-12-18 13:39:09 +02:00
9d2003d789 Update pr-agent-review.yaml 2023-12-18 13:38:06 +02:00
d2aef95847 Merge pull request #530 from Codium-ai/tr/labels
Enhancement: Implement label case conversion and update label descriptions in settings files
2023-12-18 03:21:48 -08:00
1c4e64333c feat: Implement label case conversion and update label descriptions in settings files 2023-12-18 12:29:06 +02:00
f121a420c9 Add to describe 2023-12-18 10:08:29 +02:00
3b13738943 Add docs to custom labels page 2023-12-18 10:04:05 +02:00
7a5acb29ac feat: Improved server, security and commands
Signed-off-by: Luca Simone <info@lucasimone.info>
2023-12-17 17:38:27 +01:00
ce35addcd3 Merge pull request #528 from Codium-ai/tr/lazy_init
Refactor AI handler instantiation to use lazy initialization in PR tools
2023-12-17 07:01:24 -08:00
5fb373b212 Refactor AI handler instantiation to use lazy initialization in PR tools 2023-12-17 16:52:03 +02:00
54891ad1d2 Merge pull request #514 from brianpham93/abstract-BaseAiHandler
Abstract AiHandler to BaseAiHandler
2023-12-14 07:54:13 -08:00
02871b1e3d Remove logging from pr_agent.py and add line breaks in cli.py and github_action_runner.py 2023-12-14 09:08:47 +02:00
38ea9143f3 Make LangChain dependency optional in pr-agent and update requirements.txt 2023-12-14 09:05:53 +02:00
246be6147f Set LiteLLMAIHandler as default AI handler in all PR tools and simplify AI handler injection in PRAgent 2023-12-14 09:00:14 +02:00
3531016a2c Refactor AI handler instantiation in PRAgent and related classes 2023-12-14 08:53:22 +02:00
e37598fdca Merge remote-tracking branch 'upstream/main' into abstract-BaseAiHandler 2023-12-14 07:45:43 +08:00
557b39ec87 Merge branch 'base-ai-handler' into abstract-BaseAiHandler 2023-12-14 07:44:13 +08:00
69a7c77a0d Refactor PRAgent class and has_ai_handler_param
method

This commit refactors the PRAgent class and the has_ai_handler_param
method. The has_ai_handler_param method is moved outside the class and
made a standalone function. This change improves code organization and
readability. The has_ai_handler_param function now takes a class object
as a parameter and checks if the class constructor has an "ai_handler"
parameter. This refactoring is done to streamline the code and improve
its maintainability.

No issue references.
2023-12-14 07:15:56 +08:00
2a8c2e488a Merge pull request #524 from Codium-ai/hl/native_labels_gitlab
feat: Add repository labels retrieval function in gitlab_provider.py
2023-12-13 17:36:18 +02:00
89c30ab5dc feat: Add repository labels retrieval function in gitlab_provider.py 2023-12-13 17:21:58 +02:00
ebb2ed891b Add logging to pr_agent.py 2023-12-13 08:16:45 +08:00
be8d6af87f Add code documentation generation for PR diffs 2023-12-13 08:16:31 +08:00
8fb4a42ef1 Update AI handler instantiation in server files 2023-12-13 08:16:02 +08:00
ca1ccd7b91 update base 2023-12-12 23:56:20 +08:00
b7225cc674 update langchain 2023-12-12 23:52:50 +08:00
a627dcd64f Update langchain 2023-12-12 23:28:58 +08:00
0c66554d50 langchain: move model and temperature to chat_completion 2023-12-12 23:07:46 +08:00
506eafc0c5 add langchain in requirement 2023-12-12 23:04:01 +08:00
6c7beccb4f add LangChain AI Handler 2023-12-12 23:03:49 +08:00
7eb2e769cf Move ai handlers to specific folder 2023-12-12 23:03:38 +08:00
5239e1c3e9 Load default AI Handler from util function 2023-12-12 21:51:05 +08:00
648dd3299f Merge pull request #521 from Codium-ai/tr/bitbucket_app
feat: Enable PR description publishing as comment in bitbucket_app.py
2023-12-12 00:27:48 -08:00
77a6fafdfc feat: Update Usage.md with limitations of bitbucket platform in auto_describe tool usage 2023-12-12 10:26:09 +02:00
ea7511e3c8 feat: Update Usage.md with limitations of bitbucket platform in auto_describe tool usage 2023-12-12 10:23:37 +02:00
512c92fe51 feat: Enable PR description publishing as comment in bitbucket_app.py 2023-12-12 10:19:17 +02:00
1853b4ef47 Merge pull request #520 from Codium-ai/tr/bitbucket_app
Refactor auto tool execution order and enhance logging and documentation
2023-12-12 00:02:02 -08:00
2f10b4f3c5 feat: Refactor auto tool execution order in bitbucket_app.py, add logging in pr_description.py, and update tool configuration instructions in Usage.md 2023-12-12 09:59:26 +02:00
73a20076eb Merge pull request #519 from Codium-ai/tr/bitbucket_app
Enhancement: Automatic Tool Configuration for Bitbucket App
2023-12-11 23:26:30 -08:00
afb633811f remove bad default 2023-12-12 09:18:51 +02:00
81da328ae3 feat: Add automatic tool configuration for Bitbucket app in bitbucket_app.py and configuration.toml, update Usage.md 2023-12-12 08:06:20 +02:00
729f5e9c8e Merge pull request #518 from Codium-ai/hl/github_native_labels
Refactoring Label Handling Across Git Providers
2023-12-11 16:50:19 +02:00
fdc776887d Refactor labels 2023-12-11 16:47:38 +02:00
cb64f92cce Merge pull request #511 from Codium-ai/tr/local_settings_on_push
Enhancement: Apply Repository Settings on Every 'Synchronize' Event
2023-12-11 06:27:29 -08:00
f3ad0e1d2a Merge pull request #517 from Codium-ai/tr/main_tmp
Improve PR description formatting and handling in pr_description.py
2023-12-11 06:15:46 -08:00
480e2ee678 feat: Improve PR description formatting in pr_description.py 2023-12-11 15:55:04 +02:00
9b97073174 s 2023-12-11 12:00:44 +02:00
4271bb7e52 Merge pull request #516 from Codium-ai/coditamar-readme-clarifications
Refine README.md for clarity and precision
2023-12-11 01:53:46 -08:00
e9bf8574a8 Update README.md 2023-12-11 11:52:36 +02:00
ebf7027aab add openai handler 2023-12-11 17:49:20 +08:00
a1cbd80b2a update base ai handler 2023-12-11 17:49:09 +08:00
2ce4af16cb Update README.md
fix grammar according to PR-Agent suggestions
2023-12-11 11:10:27 +02:00
2c1dfe7f3f Update README.md 2023-12-11 10:58:30 +02:00
b8021d7ca3 rename file 2023-12-11 16:57:23 +08:00
523a896465 Rename AiHandler to LiteLLMAiHandler 2023-12-11 16:56:49 +08:00
f7a6348401 Merge pull request #515 from Codium-ai/tr/review_graphics
Enhancement of Code Feedback Formatting in utils.py
2023-12-10 22:37:18 -08:00
02c0c89b13 feat: Add exception handling for discussion creation in gitlab_provider.py 2023-12-11 08:29:09 +02:00
b8cc110cbe s 2023-12-10 19:51:08 +02:00
2b1e841ef1 s 2023-12-10 19:45:54 +02:00
a247fc3263 s 2023-12-10 17:46:49 +02:00
654938f27c feat: Enhance code feedback formatting in utils.py 2023-12-10 17:30:27 +02:00
b6409929d2 Remove extra code 2023-12-09 16:51:50 +00:00
c0303ff9ec Merge remote-tracking branch 'upstream/main' into abstract-BaseAiHandler 2023-12-09 16:47:13 +00:00
f2abe5c73e Abstract AiHandler to BaseAiHandler 2023-12-09 16:39:25 +00:00
7e47baa9db Refactor AI handler classes 2023-12-10 00:25:25 +08:00
a043eb939b feat: Apply repo settings on push trigger in github_app.py 2023-12-07 08:42:18 +02:00
114 changed files with 6646 additions and 2856 deletions

32
.github/workflows/docs-ci.yaml vendored Normal file
View File

@ -0,0 +1,32 @@
name: docs-ci
on:
push:
branches:
- main
- add-docs-portal
paths:
- docs/**
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material
- run: pip install "mkdocs-material[imaging]"
- run: mkdocs gh-deploy -f docs/mkdocs.yml --force

View File

@ -5,8 +5,9 @@
name: PR-Agent name: PR-Agent
on: on:
pull_request: # pull_request:
issue_comment: # issue_comment:
workflow_dispatch:
permissions: permissions:
issues: write issues: write
@ -26,7 +27,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }} PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
GITHUB_ACTION.AUTO_REVIEW: true GITHUB_ACTION_CONFIG.AUTO_DESCRIBE: true
GITHUB_ACTION.AUTO_IMPROVE: true GITHUB_ACTION_CONFIG.AUTO_REVIEW: true
GITHUB_ACTION_CONFIG.AUTO_IMPROVE: true

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ __pycache__
dist/ dist/
*.egg-info/ *.egg-info/
build/ build/
review.md .DS_Store
docs/.cache/

View File

@ -1,5 +1,6 @@
[pr_reviewer] [pr_reviewer]
enable_review_labels_effort = true enable_review_labels_effort = true
enable_auto_approval = true
[pr_code_suggestions] [pr_code_suggestions]

View File

@ -1,462 +0,0 @@
## Installation
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.
2. A GitHub\GitLab\BitBucket personal access token (classic) with the repo scope.
There are several ways to use PR-Agent:
**Locally**
- [Using Docker image (no installation required)](INSTALL.md#use-docker-image-no-installation-required)
- [Run from source](INSTALL.md#run-from-source)
**GitHub specific methods**
- [Run as a GitHub Action](INSTALL.md#run-as-a-github-action)
- [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)
---
### Use Docker image (no installation required)
A list of the relevant tools can be found in the [tools guide](./docs/TOOLS_GUIDE.md).
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:latest --pr_url <pr_url> review
```
- For GitLab:
```
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 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
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
```
Or you can run a [specific released versions](./RELEASE_NOTES.md) of pr-agent, for example:
```
codiumai/pr-agent@v0.9
```
---
### Run from source
1. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
2. Install the requirements in your favorite virtual environment:
```
pip install -r requirements.txt
```
3. Copy the secrets template file and fill in your OpenAI key and your GitHub user token:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
chmod 600 pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
4. Add the pr_agent folder to your PYTHONPATH, then run the cli.py script:
```
export PYTHONPATH=[$PYTHONPATH:]<PATH to pr_agent folder>
python3 -m pr_agent.cli --pr_url <pr_url> review
python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
python3 -m pr_agent.cli --pr_url <pr_url> describe
python3 -m pr_agent.cli --pr_url <pr_url> 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
...
```
---
### 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 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.
Run the following command to start the server:
```
python pr_agent/servers/github_polling.py
```
---
### Run as a GitHub App
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).
- Set the following permissions:
- Pull requests: Read & write
- Issue comment: Read & write
- Metadata: Read-only
- Contents: Read-only
- Set the following events:
- Issue comment
- 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:
```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
```
3. Acquire the following pieces of information from your app's settings page:
- App private key (click "Generate a private key" and save the file)
- App ID
4. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
5. Copy the secrets template file and fill in the following:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
- Your OpenAI key.
- Copy your app's private key to the private_key field.
- Copy your app's ID to the app_id field.
- Copy your app's webhook secret to the webhook_secret field.
- Set deployment_type to 'app' in [configuration.toml](./pr_agent/settings/configuration.toml)
> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development.
> If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file.
> In most production environments, you would inject the secrets file as environment variables or as mounted volumes.
> For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following,
> assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`:
```
volumes:
- name: settings-volume
secret:
secretName: pr-agent-settings
// ...
containers:
// ...
volumeMounts:
- mountPath: /app/pr_agent/settings_prod
name: settings-volume
```
> Another option is to set the secrets as environment variables in your deployment environment, for example `OPENAI.KEY` and `GITHUB.USER_TOKEN`.
6. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
```
docker build . -t codiumai/pr-agent:github_app --target github_app -f docker/Dockerfile
docker push codiumai/pr-agent:github_app # Push to your Docker repository
```
7. Host the app using a server, serverless function, or container environment. Alternatively, for development and
debugging, you may use tools like smee.io to forward webhooks to your local machine.
You can check [Deploy as a Lambda Function](#deploy-as-a-lambda-function)
8. Go back to your app's settings, and set the following:
- Webhook URL: The URL of your app's server or the URL of the smee.io channel.
- Webhook secret: The secret you generated earlier.
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)
---
### Deploy as a Lambda Function
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
```shell
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
```
3. Push image to ECR
```shell
docker tag codiumai/pr-agent:serverless <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
docker push <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
```
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
5. Configure the lambda function to have a Function URL.
6. 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`
---
### 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:
1. Create an IAM user that you will use to read CodeCommit pull requests and post comments
* Note: That user should have CLI access only, not Console access
2. Add IAM permissions to that user, to allow access to CodeCommit (see IAM Role example below)
3. Generate an Access Key for your IAM user
4. Set the Access Key and Secret using environment variables (see Access Key example below)
5. Set the `git_provider` value to `codecommit` in the `pr_agent/settings/configuration.toml` settings file
6. Set the `PYTHONPATH` to include your `pr-agent` project directory
* Option A: Add `PYTHONPATH="/PATH/TO/PROJECTS/pr-agent` to your `.env` file
* Option B: Set `PYTHONPATH` and run the CLI in one command, for example:
* `PYTHONPATH="/PATH/TO/PROJECTS/pr-agent python pr_agent/cli.py [--ARGS]`
##### AWS CodeCommit IAM Role Example
Example IAM permissions to that user to allow access to CodeCommit:
* Note: The following is a working example of IAM permissions that has read access to the repositories and write access to allow posting comments
* Note: If you only want pr-agent to review your pull requests, you can tighten the IAM permissions further, however this IAM example will work, and allow the pr-agent to post comments to the PR
* Note: You may want to replace the `"Resource": "*"` with your list of repos, to limit access to only those repos
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:BatchDescribe*",
"codecommit:BatchGet*",
"codecommit:Describe*",
"codecommit:EvaluatePullRequestApprovalRules",
"codecommit:Get*",
"codecommit:List*",
"codecommit:PostComment*",
"codecommit:PutCommentReaction",
"codecommit:UpdatePullRequestDescription",
"codecommit:UpdatePullRequestTitle"
],
"Resource": "*"
}
]
}
```
##### AWS CodeCommit Access Key and Secret
Example setting the Access Key and Secret using environment variables
```sh
export AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXX"
export AWS_DEFAULT_REGION="us-east-1"
```
##### AWS CodeCommit CLI Example
After you set up AWS CodeCommit using the instructions above, here is an example CLI run that tells pr-agent to **review** a given pull request.
(Replace your specific PYTHONPATH and PR URL in the example)
```sh
PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
--pr_url https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/MY_REPO_NAME/pull-requests/321 \
review
```
---
### Run a GitLab webhook server
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
=======

276
README.md
View File

@ -19,45 +19,150 @@ Making pull requests less painful with an AI agent
<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>
## Table of Contents
- [News and Updates](#news-and-updates)
- [Overview](#overview)
- [Example results](#example-results)
- [Try it now](#try-it-now)
- [Installation](#installation)
- [PR-Agent Pro 💎](#pr-agent-pro-)
- [How it works](#how-it-works)
- [Why use PR-Agent?](#why-use-pr-agent)
## News and Updates
### Jan 10, 2024
- A new [knowledge-base website](https://pr-agent-docs.codium.ai/) for PR-Agent is now available. It includes detailed information about the different tools, usage guides and more, in an accessible and organized format.
### Jan 8, 2024
- A new tool, [Find Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 is now available.
<br>This tool retrieves the most similar code components from inside the organization's codebase, or from open-source code:
<kbd><a href="https://codium.ai/images/pr_agent/similar_code.mp4"><img src="https://codium.ai/images/pr_agent/similar_code_global2.png" width="512"></a></kbd>
(click on the image to see an instructional video)
### Feb 29, 2024
- You can now use the repo's [wiki page](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) to set configurations for PR-Agent 💎
<kbd><img src="https://codium.ai/images/pr_agent/wiki_configuration.png" width="512"></kbd>
## Overview
<div style="text-align:left;"> <div style="text-align:left;">
CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of commands: CodiumAI PR-Agent is an open-source tool to help efficiently review and handle pull requests.
**Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels. - See the [Installation Guide](https://pr-agent-docs.codium.ai/installation/) for instructions on installing and running the tool on different git platforms.
\
**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 ([`/ask ...`](./docs/ASK.md))**: Answering free-text questions about the PR.
\
**Code Suggestions ([`/improve`](./docs/IMPROVE.md))**: Committable code suggestions for improving the PR.
\
**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.
See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms. - See the [Usage Guide](https://pr-agent-docs.codium.ai/usage-guide/) for instructions on running the PR-Agent commands via different interfaces, including _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened.
See the [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](https://pr-agent-docs.codium.ai/tools/) for a detailed description of the different tools.
See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools. Supported commands per platform:
<h3>Example results:</h3> | | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|-------|-------------------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:--------------------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Incremental | :white_check_mark: | | | |
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Describe | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ [Inline File Summary](https://pr-agent-docs.codium.ai/tools/describe#inline-file-summary) 💎 | :white_check_mark: | | | |
| | Improve | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | :white_check_mark: | :white_check_mark: | | |
| | [Custom Suggestions](https://pr-agent-docs.codium.ai/tools/custom_suggestions/) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Find Similar Issue | :white_check_mark: | | | |
| | [Add PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | :white_check_mark: | | | |
| | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | :white_check_mark: | | | |
| | | | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | App / webhook | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Tagging bot | :white_check_mark: | | | |
| | Actions | :white_check_mark: | | :white_check_mark: | |
| | | | | | |
| CORE | PR compression | :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: |
| | Adaptive and token-aware file patch fitting | :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: |
| | [Static code analysis](https://pr-agent-docs.codium.ai/core-abilities/#static-code-analysis) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | :white_check_mark: | | | |
- 💎 means this feature is available only in [PR-Agent Pro](https://www.codium.ai/pricing/)
[//]: # (- Support for additional git providers is described in [here]&#40;./docs/Full_environments.md&#41;)
___
**Auto Description ([`/describe`](https://pr-agent-docs.codium.ai/tools/describe/))**: Automatically generating PR description - title, type, summary, code walkthrough and labels.
\
**Auto Review ([`/review`](https://pr-agent-docs.codium.ai/tools/review/))**: Adjustable feedback about the PR, possible issues, security concerns, review effort and more.
\
**Code Suggestions ([`/improve`](https://pr-agent-docs.codium.ai/tools/improve/))**: Code suggestions for improving the PR.
\
**Question Answering ([`/ask ...`](https://pr-agent-docs.codium.ai/tools/ask/))**: Answering free-text questions about the PR.
\
**Update Changelog ([`/update_changelog`](https://pr-agent-docs.codium.ai/tools/update_changelog/))**: Automatically updating the CHANGELOG.md file with the PR changes.
\
**Find Similar Issue ([`/similar_issue`](https://pr-agent-docs.codium.ai/tools/similar_issues/))**: Automatically retrieves and presents similar issues.
\
**Add Documentation 💎 ([`/add_docs`](https://pr-agent-docs.codium.ai/tools/documentation/))**: Generates documentation to methods/functions/classes that changed in the PR.
\
**Generate Custom Labels 💎 ([`/generate_labels`](https://pr-agent-docs.codium.ai/tools/custom_labels/))**: Generates custom labels for the PR, based on specific guidelines defined by the user.
\
**Analyze 💎 ([`/analyze`](https://pr-agent-docs.codium.ai/tools/analyze/))**: Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component.
\
**Custom Suggestions 💎 ([`/custom_suggestions`](https://pr-agent-docs.codium.ai/tools/custom_suggestions/))**: Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user.
\
**Generate Tests 💎 ([`/test component_name`](https://pr-agent-docs.codium.ai/tools/test/))**: Generates unit tests for a selected component, based on the PR code changes.
\
**CI Feedback 💎 ([`/checks ci_job`](https://pr-agent-docs.codium.ai/tools/ci_feedback/))**: Automatically generates feedback and analysis for a failed CI job.
\
**Similar Code 💎 ([`/find_similar_component`](https://pr-agent-docs.codium.ai/tools/similar_code//))**: Retrieves the most similar code components from inside the organization's codebase, or from open-source code.
___
## Example results
</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/530">/describe</a></h4>
<div align="center"> <div align="center">
<p float="center"> <p float="center">
<img src="https://www.codium.ai/images/describe-2.gif" width="800"> <img src="https://www.codium.ai/images/pr_agent/describe_new_short_main.png" width="512">
</p> </p>
</div> </div>
<hr>
<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/732#issuecomment-1975099151">/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"> <kbd>
<img src="https://www.codium.ai/images/pr_agent/review_new_short_main.png" width="512">
</kbd>
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/732#issuecomment-1975099159">/improve</a></h4>
<div align="center">
<p float="center">
<kbd>
<img src="https://www.codium.ai/images/pr_agent/improve_new_short_main.png" width="512">
</kbd>
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/generate_labels</a></h4>
<div align="center">
<p float="center">
<kbd><img src="https://www.codium.ai/images/pr_agent/geneare_custom_labels_main_short.png" width="300"></kbd>
</p> </p>
</div> </div>
@ -98,49 +203,14 @@ See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the dif
[//]: # (</div>) [//]: # (</div>)
<div align="left"> <div align="left">
## Table of Contents
- [Overview](#overview)
- [Try it now](#try-it-now)
- [Installation](#installation)
- [How it works](#how-it-works)
- [Why use PR-Agent?](#why-use-pr-agent)
- [Roadmap](#roadmap)
</div> </div>
<hr>
## Overview
`PR-Agent` offers extensive pull request functionalities across various git providers:
| | | GitHub | Gitlab | Bitbucket | CodeCommit | Azure DevOps | Gerrit |
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Incremental | :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: | :white_check_mark: | :white_check_mark: | :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: | :white_check_mark: | :white_check_mark: | | :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: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| | Find similar issue | :white_check_mark: | | | | | |
| | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Generate Labels | :white_check_mark: | :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: | | | |
| | Tagging bot | :white_check_mark: | | | | |
| | Actions | :white_check_mark: | | | | |
| | Web server | | | | | | :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: | | | | | |
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.
## Try it now ## Try it now
You can try GPT-4 powered PR-Agent, on your public GitHub repository, instantly. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command. Try the GPT-4 powered PR-Agent instantly on _your public GitHub repository_. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command.
For example, add a comment to any pull request with the following text: For example, add a comment to any pull request with the following text:
``` ```
@CodiumAI-Agent /review @CodiumAI-Agent /review
@ -150,30 +220,42 @@ 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 below. To set up your own PR-Agent, see the [Installation](https://pr-agent-docs.codium.ai/installation/) section below.
Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there is no need to mention `@CodiumAI-Agent ...`. Instead, directly start with the command, e.g., `/ask ...`.
--- ---
## Installation ## Installation
To use your own version of PR-Agent, 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 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 from source](INSTALL.md#method-2-run-from-source) - [Use Docker image (no installation required)](https://pr-agent-docs.codium.ai/installation/locally/#use-docker-image-no-installation-required)
- [Method 3: Run as a GitHub Action](INSTALL.md#method-3-run-as-a-github-action) - [Run from source](https://pr-agent-docs.codium.ai/installation/locally/#run-from-source)
- [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 **GitHub specific methods**
- [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app) - [Run as a GitHub Action](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-action)
- Allowing you to automate the review process on your private or public repositories - [Run as a GitHub App](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-app)
- [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) **GitLab specific methods**
- [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server) - [Run a GitLab webhook server](https://pr-agent-docs.codium.ai/installation/gitlab/)
- [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline)
**BitBucket specific methods**
- [Run as a Bitbucket Pipeline](https://pr-agent-docs.codium.ai/installation/bitbucket/)
## PR-Agent Pro 💎
[PR-Agent Pro](https://www.codium.ai/pricing/) is a hosted version of PR-Agent, provided by CodiumAI. It is available for a monthly fee, and provides the following benefits:
1. **Fully managed** - We take care of everything for you - hosting, models, regular updates, and more. Installation is as simple as signing up and adding the PR-Agent app to your GitHub\BitBucket repo.
2. **Improved privacy** - No data will be stored or used to train models. PR-Agent Pro will employ zero data retention, and will use an OpenAI account with zero data retention.
3. **Improved support** - PR-Agent Pro users will receive priority support, and will be able to request new features and capabilities.
4. **Extra features** -In addition to the benefits listed above, PR-Agent Pro will emphasize more customization, and the usage of static code analysis, in addition to LLM logic, to improve results.
See [here](https://pr-agent-docs.codium.ai/#pr-agent-pro) for a list of features available in PR-Agent Pro.
## How it works ## How it works
@ -181,54 +263,26 @@ The following diagram illustrates PR-Agent tools and their flow:
![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png) ![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png)
Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt Check out the [PR Compression strategy](https://pr-agent-docs.codium.ai/core-abilities/#pr-compression-strategy) page for more details on how we convert a code diff to a manageable LLM prompt
## Why use PR-Agent? ## Why use PR-Agent?
A reasonable question that can be asked is: `"Why use PR-Agent? What make it stand out from existing tools?"` A reasonable question that can be asked is: `"Why use PR-Agent? What makes it stand out from existing tools?"`
Here are some advantages of PR-Agent: 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](https://pr-agent-docs.codium.ai/core-abilities/#pr-compression-strategy) 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](pr_agent/settings/configuration.toml) 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), **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.
## Roadmap ## Data privacy
- [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172)) If you host PR-Agent with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here:
- [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)
- [x] PR-Agent for issues
- [ ] Adding more tools. Possible directions:
- [x] PR description
- [x] Inline code suggestions
- [x] Reflect and review
- [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89))
- [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues)
- [x] Documentation (is the PR properly documented)
- [ ] ...
See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes.
## Similar Projects
- [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)
- [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer)
- [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview)
- [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 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. When using PR-Agent Pro 💎, hosted by CodiumAI, we will not store any of your data, nor will we use it for training.
You will also benefit from an OpenAI account with zero data retention. You will also benefit from an OpenAI account with zero data retention.
## Links ## Links

419
Usage.md
View File

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

@ -26,6 +26,10 @@ FROM base as gitlab_webhook
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/gitlab_webhook.py"] CMD ["python", "pr_agent/servers/gitlab_webhook.py"]
FROM base as azure_devops_webhook
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/azuredevops_server_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

View File

@ -1,15 +0,0 @@
# 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 ...".

View File

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

View File

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

@ -1,41 +0,0 @@
# 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"
```

View File

@ -1,57 +0,0 @@
# 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.

15
docs/README.md Normal file
View File

@ -0,0 +1,15 @@
# To install:
pip install mkdocs
pip install mkdocs-material
pip install mkdocs-material-extensions
pip install "mkdocs-material[imaging]"
# docs
To run localy: `mkdocs serve`
To expand and customize the theme: [Material MKDocs](https://squidfunk.github.io/mkdocs-material/)
The deployment is managed on the gh-pages branches.
After each merge to main the deplloyment will be taken care of by GH action automatically and the new version will be available at: [Docs](https://codium-ai.github.io/docs/)
Github action is located in `.github/workflows/ci.yml` file.

View File

@ -1,79 +0,0 @@
# 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.

View File

@ -1,11 +0,0 @@
## 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.

1
docs/docs/CNAME Normal file
View File

@ -0,0 +1 @@
pr-agent-docs.codium.ai

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/docs/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -1,42 +1,52 @@
# PR Compression Strategy ## 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)
For both scenarios, we first use the following strategy For both scenarios, we first use the following strategy
#### Repo language prioritization strategy
#### Repo language prioritization strategy
We prioritize the languages of the repo based on the following criteria: We prioritize the languages of the repo based on the following criteria:
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. Given the main languages used in the repo 2. Given the main languages used in the repo
2. We sort the PR files by the most common languages in the repo (in descending order): 3. We sort the PR files by the most common languages in the repo (in descending order):
* ```[[file.py, file2.py],[file3.js, file4.jsx],[readme.md]]``` * ```[[file.py, file2.py],[file3.js, file4.jsx],[readme.md]]```
## 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 3 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
### Motivation ### Large PR
#### Motivation
Pull Requests can be very long and contain a lot of information with varying degree of relevance to the pr-agent. Pull Requests can be very long and contain a lot of information with varying degree of relevance to the pr-agent.
We want to be able to pack as much information as possible in a single LMM prompt, while keeping the information relevant to the pr-agent. We want to be able to pack as much information as possible in a single LMM prompt, while keeping the information relevant to the pr-agent.
#### 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
#### Adaptive and token-aware file patch fitting #### Adaptive and token-aware file patch fitting
We use [tiktoken](https://github.com/openai/tiktoken) to tokenize the patches after the modifications described above, and we use the following strategy to fit the patches into the prompt: We use [tiktoken](https://github.com/openai/tiktoken) to tokenize the patches after the modifications described above, and we use the following strategy to fit the patches into the prompt:
1. Within each language we sort the files by the number of tokens in the file (in descending order):
* ```[[file2.py, file.py],[file4.jsx, file3.js],[readme.md]]```
2. Iterate through the patches in the order described above
2. Add the patches to the prompt until the prompt reaches a certain buffer from the max token length
3. If there are still patches left, add the remaining patches as a list called `other modified 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 1. Within each language we sort the files by the number of tokens in the file (in descending order):
- ```[[file2.py, file.py],[file4.jsx, file3.js],[readme.md]]```
2. Iterate through the patches in the order described above
3. Add the patches to the prompt until the prompt reaches a certain buffer from the max token length
4. If there are still patches left, add the remaining patches as a list called `other modified files` to the prompt until the prompt reaches the max token length (hard stop), skip the rest of the patches.
5. 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
<kbd><img src=https://codium.ai/images/git_patch_logic.png width="768"></kbd> <kbd><img src=https://codium.ai/images/git_patch_logic.png width="768"></kbd>
## YAML Prompting
TBD
## Static Code Analysis 💎
TBD

98
docs/docs/css/custom.css Normal file
View File

@ -0,0 +1,98 @@
:root {
--md-primary-fg-color: #765bfa;
--md-accent-fg-color: #AEA1F1;
}
.md-nav__title, .md-nav__link {
font-size: 16px; /* Adjust the font size as needed */
}
.md-tabs__link {
font-size: 16px; /* Adjust the font size as needed */
}
.md-header__title {
font-size: 20px; /* Adjust the font size as needed */
}
/*
@media (prefers-color-scheme: light) {
body {
--md-primary-fg-color: #00ffee !important;
--md-primary-bg-color: #ff0000 !important;
}
body, .md-main, .md-content {
background-color: #4312f5 !important;
}
}
@media (prefers-color-scheme: dark) {
body {
--md-primary-fg-color: #171518 !important;
--md-primary-bg-color: #171518 !important;
}
body, .md-main, .md-content {
background-color: #171518 !important;
}
.md-header__title {
color: #ffffff !important;
}
.md-tabs .md-tabs__link {
color: #ffffff !important;
}
.md-tabs .md-tabs__link:hover,
.md-tabs .md-tabs__link:focus {
color: #ffffff !important;
}
.md-header__button {
color: #ffffff !important;
}
.md-header__button svg {
fill: currentColor !important;
}
.md-header__button:hover,
.md-header__button:focus {
color: #ffffff !important;
}
.md-header__button:hover svg,
.md-header__button:focus svg {
fill: currentColor !important;
}
.md-search__icon svg {
fill: #ffffff !important;
}
.md-search__input {
color: #ffffff !important;
}
.md-nav__item--active > .md-nav__link--active,
.md-nav__link--active {
color: #AEA1F1 !important;
}
.md-nav--secondary .md-nav__title {
background: #171518;
box-shadow: 0 0 0.4rem 0.4rem #171518;
}
.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link {
background: #171518;
box-shadow: 0 0 0.4rem 0.4rem #171518;
}
.md-content a {
color: #AEA1F1 !important;
}
} */

162
docs/docs/index.md Normal file
View File

@ -0,0 +1,162 @@
# Overview
CodiumAI PR-Agent is an open-source tool to help efficiently review and handle pull requests.
- See the [Installation Guide](./installation/index.md) for instructions on installing and running the tool on different git platforms.
- See the [Usage Guide](./usage-guide/index.md) for instructions on running the PR-Agent commands via different interfaces, including _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened.
- See the [Tools Guide](./tools/index.md) for a detailed description of the different tools.
## PR-Agent Features
PR-Agent offers extensive pull request functionalities across various git providers.
| | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|-------|---------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
| TOOLS | Review | ✔️ | ✔️ | ✔️ | ✔️ |
| | ⮑ Incremental | ✔️ | | | |
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | Ask | ✔️ | ✔️ | ✔️ | ✔️ |
| | Describe | ✔️ | ✔️ | ✔️ | ✔️ |
| | ⮑ [Inline file summary](https://pr-agent-docs.codium.ai/tools/describe/#inline-file-summary){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | Improve | ✔️ | ✔️ | ✔️ | ✔️ |
| | ⮑ Extended | ✔️ | ✔️ | ✔️ | ✔️ |
| | [Custom Suggestions](./tools/custom_suggestions.md){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | Reflect and Review | ✔️ | ✔️ | ✔️ | ✔️ |
| | Update CHANGELOG.md | ✔️ | ✔️ | ✔️ | |
| | Find Similar Issue | ✔️ | | | |
| | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | [Generate Custom Labels](./tools/describe.md#handle-custom-labels-from-the-repos-labels-page-💎){:target="_blank"} 💎 | ✔️ | ✔️ | | ✔️ |
| | [Analyze PR Components](./tools/analyze.md){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | | | | | |
| USAGE | CLI | ✔️ | ✔️ | ✔️ | ✔️ |
| | App / webhook | ✔️ | ✔️ | | ✔️ |
| | Tagging bot | ✔️ | | | ✔️ |
| | Actions | ✔️ | | | |
| | | | | |
| CORE | PR compression | ✔️ | ✔️ | ✔️ | ✔️ |
| | Repo language prioritization | ✔️ | ✔️ | ✔️ | ✔️ |
| | Adaptive and token-aware file patch fitting | ✔️ | ✔️ | ✔️ | ✔️ |
| | Multiple models support | ✔️ | ✔️ | ✔️ | ✔️ |
| | Incremental PR review | ✔️ | | | |
| | [Static code analysis](./tools/analyze.md/){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
| | [Multiple configuration options](./usage-guide/configuration_options.md){:target="_blank"} 💎 | ✔️ | ✔️ | ✔️ | ✔️ |
💎 marks a feature available only in [PR-Agent Pro](https://www.codium.ai/pricing/){:target="_blank"}
## Example results
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/describe</a></h4>
<div align="center">
<p float="center">
<img src="https://www.codium.ai/images/pr_agent/describe_new_short_main.png" width="512">
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/732#issuecomment-1975099151">/review</a></h4>
<div align="center">
<p float="center">
<kbd>
<img src="https://www.codium.ai/images/pr_agent/review_new_short_main.png" width="512">
</kbd>
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/732#issuecomment-1975099159">/improve</a></h4>
<div align="center">
<p float="center">
<kbd>
<img src="https://www.codium.ai/images/pr_agent/improve_new_short_main.png" width="512">
</kbd>
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/generate_labels</a></h4>
<div align="center">
<p float="center">
<kbd><img src="https://www.codium.ai/images/pr_agent/geneare_custom_labels_main_short.png" width="300"></kbd>
</p>
</div>
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>)
[//]: # (<div align="center">)
[//]: # (<p float="center">)
[//]: # (<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800">)
[//]: # (</p>)
[//]: # (</div>)
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>)
[//]: # (<div align="center">)
[//]: # (<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>
<hr>
## How it works
The following diagram illustrates PR-Agent tools and their flow:
![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png)
Check out the [PR Compression strategy](core-abilities/index.md) page for more details on how we convert a code diff to a manageable LLM prompt
## PR-Agent Pro 💎
[PR-Agent Pro](https://www.codium.ai/pricing/) is a hosted version of PR-Agent, provided by CodiumAI. It is available for a monthly fee, and provides the following benefits:
1. **Fully managed** - We take care of everything for you - hosting, models, regular updates, and more. Installation is as simple as signing up and adding the PR-Agent app to your GitHub\BitBucket repo.
2. **Improved privacy** - No data will be stored or used to train models. PR-Agent Pro will employ zero data retention, and will use an OpenAI account with zero data retention.
3. **Improved support** - PR-Agent Pro users will receive priority support, and will be able to request new features and capabilities.
4. **Extra features** -In addition to the benefits listed above, PR-Agent Pro will emphasize more customization, and the usage of static code analysis, in addition to LLM logic, to improve results. It has the following additional tools and features:
- [**Analyze PR components**](./tools/analyze.md/)
- [**Custom Code Suggestions**](./tools/custom_suggestions.md/)
- [**Tests**](./tools/test.md/)
- [**PR documentation**](./tools/documentation.md/)
- [**CI feedback**](./tools/ci_feedback.md/)
- [**SOC2 compliance check**](./tools/review.md/#soc2-ticket-compliance)
- [**Custom labels**](./tools/describe.md/#handle-custom-labels-from-the-repos-labels-page)
- [**Global and wiki configuration**](./usage-guide/configuration_options.md/#wiki-configuration-file)
## Data privacy
If you host 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 PR-Agent Pro 💎, hosted by CodiumAI, we will not store any of your data, nor will we use it for training.
You will also benefit from an OpenAI account with zero data retention.

View File

@ -0,0 +1,36 @@
## Azure DevOps provider
To use Azure DevOps provider use the following settings in configuration.toml:
```
[config]
git_provider="azure"
use_repo_settings_file=false
```
Azure DevOps provider supports [PAT token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows) or [DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-overview#authentication-in-server-environments) authentication.
PAT is faster to create, but has build in experation date, and will use the user identity for API calls.
Using DefaultAzureCredential you can use managed identity or Service principle, which are more secure and will create seperate ADO user identity (via AAD) to the agent.
If PAT was choosen, you can assign the value in .secrets.toml.
If DefaultAzureCredential was choosen, you can assigned the additional env vars like AZURE_CLIENT_SECRET directly,
or use managed identity/az cli (for local develpment) without any additional configuration.
in any case, 'org' value must be assigned in .secrets.toml:
```
[azure_devops]
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
# pat = "YOUR_PAT_TOKEN" needed only if using PAT for authentication
```
### Azure DevOps Webhook
To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops).
Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with /<command> <args> comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported.
For webhook security, create a sporadic username/password pair and configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request:
```
[azure_devops_server]
webhook_username = "<basic auth user>"
webhook_password = "<basic auth password>"
```
> :warning: **Ensure that the webhook endpoint is only accessible over HTTPS** to mitigate the risk of credential interception when using basic authentication.

View File

@ -0,0 +1,70 @@
## 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](mailto: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

@ -0,0 +1,249 @@
## 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 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](https://github.com/Codium-ai/pr-agent/blob/main/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 GitHub App
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).
- Set the following permissions:
- Pull requests: Read & write
- Issue comment: Read & write
- Metadata: Read-only
- Contents: Read-only
- Set the following events:
- Issue comment
- 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:
```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
```
3) Acquire the following pieces of information from your app's settings page:
- App private key (click "Generate a private key" and save the file)
- App ID
4) Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
5) Copy the secrets template file and fill in the following:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
- Your OpenAI key.
- Copy your app's private key to the private_key field.
- Copy your app's ID to the app_id field.
- Copy your app's webhook secret to the webhook_secret field.
- Set deployment_type to 'app' in [configuration.toml](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml)
> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development.
> If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file.
> In most production environments, you would inject the secrets file as environment variables or as mounted volumes.
> For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following,
> assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`:
```
volumes:
- name: settings-volume
secret:
secretName: pr-agent-settings
// ...
containers:
// ...
volumeMounts:
- mountPath: /app/pr_agent/settings_prod
name: settings-volume
```
> Another option is to set the secrets as environment variables in your deployment environment, for example `OPENAI.KEY` and `GITHUB.USER_TOKEN`.
6) Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
```
docker build . -t codiumai/pr-agent:github_app --target github_app -f docker/Dockerfile
docker push codiumai/pr-agent:github_app # Push to your Docker repository
```
7. Host the app using a server, serverless function, or container environment. Alternatively, for development and
debugging, you may use tools like smee.io to forward webhooks to your local machine.
You can check [Deploy as a Lambda Function](#deploy-as-a-lambda-function)
8. Go back to your app's settings, and set the following:
- Webhook URL: The URL of your app's server or the URL of the smee.io channel.
- Webhook secret: The secret you generated earlier.
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.
> However, you can override the default tool parameters by uploading a local configuration file `.pr_agent.toml`
> For more information please check out the [USAGE GUIDE](../usage-guide/automations_and_usage.md#github-app)
---
## Deploy as a Lambda Function
1. Follow steps 1-5 from [here](#run-as-a-github-app).
2. Build a docker image that can be used as a lambda function
```shell
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
```
3. Push image to ECR
```shell
docker tag codiumai/pr-agent:serverless <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
docker push <AWS_ACCOUNT>.dkr.ecr.<AWS_REGION>.amazonaws.com/codiumai/pr-agent:serverless
```
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
5. Configure the lambda function to have a Function URL.
6. 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`
---
## 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:
1. Create an IAM user that you will use to read CodeCommit pull requests and post comments
* Note: That user should have CLI access only, not Console access
2. Add IAM permissions to that user, to allow access to CodeCommit (see IAM Role example below)
3. Generate an Access Key for your IAM user
4. Set the Access Key and Secret using environment variables (see Access Key example below)
5. Set the `git_provider` value to `codecommit` in the `pr_agent/settings/configuration.toml` settings file
6. Set the `PYTHONPATH` to include your `pr-agent` project directory
* Option A: Add `PYTHONPATH="/PATH/TO/PROJECTS/pr-agent` to your `.env` file
* Option B: Set `PYTHONPATH` and run the CLI in one command, for example:
* `PYTHONPATH="/PATH/TO/PROJECTS/pr-agent python pr_agent/cli.py [--ARGS]`
---
#### AWS CodeCommit IAM Role Example
Example IAM permissions to that user to allow access to CodeCommit:
* Note: The following is a working example of IAM permissions that has read access to the repositories and write access to allow posting comments
* Note: If you only want pr-agent to review your pull requests, you can tighten the IAM permissions further, however this IAM example will work, and allow the pr-agent to post comments to the PR
* Note: You may want to replace the `"Resource": "*"` with your list of repos, to limit access to only those repos
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:BatchDescribe*",
"codecommit:BatchGet*",
"codecommit:Describe*",
"codecommit:EvaluatePullRequestApprovalRules",
"codecommit:Get*",
"codecommit:List*",
"codecommit:PostComment*",
"codecommit:PutCommentReaction",
"codecommit:UpdatePullRequestDescription",
"codecommit:UpdatePullRequestTitle"
],
"Resource": "*"
}
]
}
```
#### AWS CodeCommit Access Key and Secret
Example setting the Access Key and Secret using environment variables
```sh
export AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXX"
export AWS_DEFAULT_REGION="us-east-1"
```
#### AWS CodeCommit CLI Example
After you set up AWS CodeCommit using the instructions above, here is an example CLI run that tells pr-agent to **review** a given pull request.
(Replace your specific PYTHONPATH and PR URL in the example)
```sh
PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
--pr_url https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/MY_REPO_NAME/pull-requests/321 \
review
```

View File

@ -0,0 +1,20 @@
## Run a GitLab webhook server
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 [here](https://pr-agent-docs.codium.ai/installation/github/#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](https://github.com/Codium-ai/pr-agent/blob/main/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.

View File

@ -0,0 +1,19 @@
# Installation
If you choose to host you own PR-Agent, you first need to acquire two tokens:
1. An OpenAI key from [here](https://platform.openai.com/api-keys), with access to GPT-4 (or a key for [other models](../usage-guide/additional_configurations.md/#changing-a-model), if you prefer).
2. A GitHub\GitLab\BitBucket personal access token (classic), with the repo scope. [GitHub from [here](https://github.com/settings/tokens)]
There are several ways to use self-hosted PR-Agent:
- [Locally](./locally.md)
- [GitHub](./github.md)
- [GitLab](./gitlab.md)
- [BitBucket](./bitbucket.md)
- [Azure DevOps](./azure.md)
___
Note that [PR-Agent Pro 💎](https://app.codium.ai/), an app for GitHub\GitLab\BitBucket hosted by CodiumAI, is also available.
<br>
With PR-Agent Pro Installation is as simple as signing up and adding the PR-Agent app to your relevant repo.

View File

@ -0,0 +1,84 @@
## Use Docker image (no installation required)
A list of the relevant tools can be found in the [tools guide](../tools/ask.md).
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:latest --pr_url <pr_url> review
```
- For GitLab:
```
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 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> -e 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
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
```
Or you can run a [specific released versions](https://github.com/Codium-ai/pr-agent/blob/main/RELEASE_NOTES.md) of pr-agent, for example:
```
codiumai/pr-agent@v0.9
```
---
## Run from source
1. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
2. Navigate to the `/pr-agent` folder and install the requirements in your favorite virtual environment:
```
pip install -e .
```
*Note: If you get an error related to Rust in the dependency installation then make sure Rust is installed and in your `PATH`, instructions: https://rustup.rs*
3. Copy the secrets template file and fill in your OpenAI key and your GitHub user token:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
chmod 600 pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
4. Run the cli.py script:
```
python3 -m pr_agent.cli --pr_url <pr_url> review
python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
python3 -m pr_agent.cli --pr_url <pr_url> describe
python3 -m pr_agent.cli --pr_url <pr_url> 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
...
```
[Optional] Add the pr_agent folder to your PYTHONPATH
```
export PYTHONPATH=$PYTHONPATH:<PATH to pr_agent folder>
```

View File

@ -0,0 +1,24 @@
## Overview
The `analyze` tool combines static code analysis with LLM capabilities to provide a comprehensive analysis of the PR code changes.
The tool scans the PR code changes, find the code components (methods, functions, classes) that changed, and summarizes the changes in each component.
It can be invoked manually by commenting on any PR:
```
/analyze
```
## Example usage
An example [result](https://github.com/Codium-ai/pr-agent/pull/546#issuecomment-1868524805):
<kbd><img src=https://codium.ai/images/pr_agent/analyze_1.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/analyze_2.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/analyze_3.png width="768"></kbd>
**Notes**
- Language that are currently supported: Python, Java, C++, JavaScript, TypeScript.

23
docs/docs/tools/ask.md Normal file
View File

@ -0,0 +1,23 @@
## Overview
The `ask` tool answers questions about the PR, based on the PR code changes. Make sure to be specific and clear in your questions.
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>
## Ask lines
You can run `/ask` on specific lines of code in the PR from the PR's diff view. The tool will answer questions based on the code changes in the selected lines.
- Click on the '+' sign next to the line number to select the line.
- To select multiple lines, click on the '+' sign of the first line and then hold and drag to select the rest of the lines.
- write `/ask "..."` in the comment box and press `Add single comment` button.
<kbd><img src="https://codium.ai/images/pr_agent/Ask_line.png" width="768"></kbd>
Note that the tool does not have "memory" of previous questions, and answers each question independently.

View File

@ -0,0 +1,32 @@
## Overview
The CI feedback tool (`/checks)` automatically triggers when a PR has a failed check.
The tool analyzes the failed checks and provides several feedbacks:
- Failed stage
- Failed test name
- Failure summary
- Relevant error logs
<kbd>
<img src="https://www.codium.ai/images/pr_agent/failed_check1.png" width="768">
</kbd>
&rarr;
<kbd>
<img src="https://www.codium.ai/images/pr_agent/failed_check2.png" width="768">
</kbd>
___
In addition to being automatically triggered, the tool can also be invoked manually by commenting on a PR:
```
/checks "https://github.com/{repo_name}/actions/runs/{run_number}/job/{job_number}"
```
where `{repo_name}` is the name of the repository, `{run_number}` is the run number of the failed check, and `{job_number}` is the job number of the failed check.
## Configuration options
- `enable_auto_checks_feedback` - if set to true, the tool will automatically provide feedback when a check is failed. Default is true.
- `excluded_checks_list` - a list of checks to exclude from the feedback, for example: ["check1", "check2"]. Default is an empty list.
- `persistent_comment` - if set to true, the tool will overwrite a previous checks comment with the new feedback. Default is true.
- `enable_help_text=true` - if set to true, the tool will provide a help message when a user comments "/checks" on a PR. Default is true.
- `final_update_message` - if `persistent_comment` is true and updating a previous checks message, the tool will also create a new message: "Persistent checks updated to latest commit". Default is true.

View File

@ -0,0 +1,58 @@
## Overview
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>
Note that in addition to the dedicated tool `generate_labels`, the custom labels will also be used by the `describe` tool.
### How to enable custom labels
There are 3 ways to enable custom labels:
#### 1. CLI (local configuration file)
When working from CLI, you need to apply the [configuration changes](#configuration-options) to the [custom_labels file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/custom_labels.toml):
#### 2. Repo configuration file
To enable custom labels, you need to apply the [configuration changes](#configuration-options) to the local `.pr_agent.toml` file in you repository.
#### 3. Handle custom labels from the Repo's labels page 💎
> This feature is available only in PR-Agent Pro
* GitHub : `https://github.com/{owner}/{repo}/labels`, or click on the "Labels" tab in the issues or PRs page.
* GitLab : `https://gitlab.com/{owner}/{repo}/-/labels`, or click on "Manage" -> "Labels" on the left menu.
b. Add/edit the custom labels. It should be formatted as follows:
* Label name: The name of the custom label.
* Description: Start the description of with prefix `pr_agent:`, for example: `pr_agent: Description of when AI should suggest this label`.<br>
The description should be comprehensive and detailed, indicating when to add the desired label.
<kbd><img src=https://codium.ai/images/pr_agent/add_native_custom_labels.png width="880"></kbd>
c. Now the custom labels will be included in the `generate_labels` tool.
> This feature is supported in GitHub and GitLab.
## Configuration options
- 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"
```

View File

@ -0,0 +1,57 @@
## Overview
The `custom_suggestions` tool scans the PR code changes, and automatically generates custom suggestions for improving the PR code.
It shares similarities with the `improve` tool, but with one main difference: the `custom_suggestions` tool will only propose suggestions that follow specific guidelines defined by the prompt in: `pr_custom_suggestions.prompt` configuration.
The tool can be triggered [automatically](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened) every time a new PR is opened, or can be invoked manually by commenting on a PR.
When commenting, use the following template:
```
/custom_suggestions --pr_custom_suggestions.prompt="The suggestions should focus only on the following:\n-...\n-...\n-..."
```
With a [configuration file](../usage-guide/automations_and_usage.md#github-app), use the following template:
```
[pr_custom_suggestions]
prompt="""\
The suggestions should focus only on the following:
-...
-...
-...
"""
```
Using a configuration file is recommended, since it allows to use multi-line instructions.
Don't forget - with this tool, you are the prompter. Be specific, clear, and concise in the instructions. Specify relevant aspects that you want the model to focus on. \
You might benefit from several trial-and-error iterations, until you get the correct prompt for your use case.
## Example usage
Here is an example of a possible prompt:
```
[pr_custom_suggestions]
prompt="""\
The suggestions should focus only on the following:
- look for edge cases when implementing a new function
- make sure every variable has a meaningful name
- make sure the code is efficient
"""
```
The instructions above are just an example. We want to emphasize that the prompt should be specific and clear, and be tailored to the needs of your project.
Results obtained with the prompt above:
<kbd><img src=https://codium.ai/images/pr_agent/custom_suggestions_prompt.png width="512"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/custom_suggestions_result.png width="768"></kbd>
## Configuration options
`prompt`: the prompt for the tool. It should be a multi-line string.
`num_code_suggestions`: number of code suggestions provided by the 'custom_suggestions' tool. Default is 4.
`enable_help_text`: if set to true, the tool will display a help text in the comment. Default is true.

153
docs/docs/tools/describe.md Normal file
View File

@ -0,0 +1,153 @@
## Overview
The `describe` tool scans the PR code changes, and generates a description for the PR - title, type, summary, walkthrough and labels.
The tool can be triggered automatically every time a new PR is [opened](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened), or 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_new.png width="768"></kbd>
## Configuration options
To edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L46) related to the describe tool (`pr_description` section), use the following template:
```
/describe --pr_description.some_config1=... --pr_description.some_config2=...
```
### Possible configurations:
- `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 true.
- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is true.
- `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](./custom_labels.md#configuration-options)
- `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, "Changes walkthrough" section will be generated. Default is true.
- `collapsible_file_list`: if set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".
### Inline file summary 💎
> This feature is available only in PR-Agent Pro
This feature will enable you to quickly understand the changes in each file while reviewing the code changes (diff view).
To add the walkthrough table to the "Files changed" tab, you can click on the checkbox that appears PR Description status message below the main PR Description:
<kbd><img src=https://codium.ai/images/pr_agent/add_table_checkbox.png width="512"></kbd>
If you prefer to have the file summaries appear in the "Files changed" tab on every PR, change the `pr_description.inline_file_summary` parameter in the configuration file, possible values are:
- `'table'`: File changes walkthrough table will be displayed on the top of the "Files changed" tab, in addition to the "Conversation" tab.
<kbd><img src=https://codium.ai/images/pr_agent/diffview-table.png width="768"></kbd>
- `true`: A collapsable file comment with changes title and a changes summary for each file in the PR.
<kbd><img src=https://codium.ai/images/pr_agent/diffview_changes.png width="768"></kbd>
- `false` (`default`): File changes walkthrough will be added only to the "Conversation" tab.
**Note** that this feature is currently available only for GitHub.
### Handle custom labels from the Repo's labels page 💎
> This feature is available only in PR-Agent Pro
You can control the custom labels that will be suggested by the `describe` tool, from the repo's labels page:
* GitHub : go to `https://github.com/{owner}/{repo}/labels` (or click on the "Labels" tab in the issues or PRs page)
* GitLab : go to `https://gitlab.com/{owner}/{repo}/-/labels` (or click on "Manage" -> "Labels" on the left menu)
Now add/edit the custom labels. they should be formatted as follows:
* Label name: The name of the custom label.
* Description: Start the description of with prefix `pr_agent:`, for example: `pr_agent: Description of when AI should suggest this label`.<br>
The description should be comprehensive and detailed, indicating when to add the desired label. For example:
<kbd><img src=https://codium.ai/images/pr_agent/add_native_custom_labels.png width="880"></kbd>
### Markers template
To enable markers, set `pr_description.use_description_markers=true`.
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 Type:
pr_agent:type
## PR Description:
pr_agent:summary
## PR Walkthrough:
pr_agent:walkthrough
```
The marker `pr_agent:type` will be replaced with the PR type, `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
<kbd><img src=https://codium.ai/images/pr_agent/describe_markers_before.png width="768"></kbd>
&rarr;
<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.
## Usage Tips
### Automation
- When you first install the app, the [default mode](../usage-guide/automations_and_usage.md#github-app) for the describe tool is:
```
pr_commands = ["/describe --pr_description.add_original_user_description=true"
"--pr_description.keep_original_user_title=true", ...]
```
meaning the `describe` tool will run automatically on every PR, will keep the original title, and will add the original user description above the generated description.
<br> This default settings aim to strike a good balance between automation and control:
If you want more automation, just give the PR a title, and the tool will auto-write a full description; If you want more control, you can add a detailed description, and the tool will add the complementary description below it.
- For maximal automation, you can change the default mode to:
```
pr_commands = ["/describe --pr_description.add_original_user_description=false"
"--pr_description.keep_original_user_title=true", ...]
```
so the title will be auto-generated as well.
- Markers are an alternative way to control the generated description, to give maximal control to the user. If you set:
```
pr_commands = ["/describe --pr_description.use_description_markers=true", ...]
```
the tool will replace every marker of the form `pr_agent:marker_name` in the PR description with the relevant content, where `marker_name` is one of the following:
- `type`: the PR type.
- `summary`: the PR summary.
- `walkthrough`: the PR walkthrough.
Note that when markers are enabled, if the original PR description does not contain any markers, the tool will not alter the description at all.
### Custom labels
The default labels of the describe tool are quite generic, since they are meant to be used in any repo: [`Bug fix`, `Tests`, `Enhancement`, `Documentation`, `Other`].
If you specify [custom labels](#handle-custom-labels-from-the-repos-labels-page) in the repo's labels page, you can get tailored labels for your use cases.
Examples for custom labels:
- `Main topic:performance` - pr_agent:The main topic of this PR is performance
- `New endpoint` - pr_agent:A new endpoint was added in this PR
- `SQL query` - pr_agent:A new SQL query was added in this PR
- `Dockerfile changes` - pr_agent:The PR contains changes in the Dockerfile
- ...
The list above is eclectic, and aims to give an idea of different possibilities. Define custom labels that are relevant for your repo and use cases.
Note that Labels are not mutually exclusive, so you can add multiple label categories.
<br>Make sure to provide proper title, and a detailed and well-phrased description for each label, so the tool will know when to suggest it.

View File

@ -0,0 +1,24 @@
## Overview
The `add_docs` tool scans the PR code changes, and automatically suggests documentation for any code components that changed in the PR (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/docs_command.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/docs_components.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/docs_single_component.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 ...".
**Notes**
- Language that are currently fully supported: Python, Java, C++, JavaScript, TypeScript.
- For languages that are not fully supported, the tool will suggest documentation only for new components in the PR.
- A previous version of the tool, that offered support only for new components, was deprecated.

View File

@ -0,0 +1,89 @@
## Overview
The `improve` tool scans the PR code changes, and automatically generates suggestions for improving the PR code.
The tool can be triggered automatically every time a new PR is [opened](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened), or it can be invoked manually by commenting on any PR:
```
/improve
```
### Summarized vs committable code suggestions
The code suggestions can be presented as a single comment (via `pr_code_suggestions.summarize=true`):
<kbd><img src=https://codium.ai/images/pr_agent/code_suggestions_as_comment.png width="768"></kbd>
Or as a separate commitable code comment for each suggestion:
<kbd><img src=https://codium.ai/images/pr_agent/improve.png width="768"></kbd>
Note that a single comment has a significantly smaller PR footprint. We recommend this mode for most cases.
Also note that collapsible are not supported in _Bitbucket_. Hence, the suggestions are presented there as code comments.
### Extended mode
An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR:
```
/improve --extended
```
or by setting:
```
[pr_code_suggestions]
auto_extended_mode=true
```
(True by default).
Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (might use multiple calls to GPT-4 for large PRs).
Hence, the total number of suggestions is proportional to the number of chunks, i.e., the size of the PR.
### Configuration options
To edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L66) related to the improve tool (`pr_code_suggestions` section), use the following template:
```
/improve --pr_code_suggestions.some_config1=... --pr_code_suggestions.some_config2=...
```
### General options
- `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.
- `summarize`: if set to true, the tool will display the suggestions in a single comment. Default is false.
- `enable_help_text`: if set to true, the tool will display a help text in the comment. Default is true.
### params for '/improve --extended' mode
- `auto_extended_mode`: enable extended mode automatically (no need for the `--extended` option). Default is true.
- `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.
## Usage Tips
### Extra instructions
Extra instructions are very important for the `imrpove` tool, since they enable you to guide the model to suggestions that are more relevant to the specific needs of the project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify relevant aspects that you want the model to focus on.
Examples for extra instructions:
```
[pr_code_suggestions] # /improve #
extra_instructions="""
Emphasize the following aspects:
- Does the code logic cover relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
### 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](https://github.com/Codium-ai/pr-agent/blob/main/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 `exra_instructions` field to guide the model to suggestions that are more relevant to the specific needs of the project.
- Consider also trying the [Custom Suggestions Tool](./custom_suggestions.md) 💎, that will **only** propose suggestions that follow specific guidelines defined by user.

20
docs/docs/tools/index.md Normal file
View File

@ -0,0 +1,20 @@
# Tools
Here is a list of PR-Agent tools, each with a dedicated page that explains how to use it:
| Tool | Description |
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| **[PR Description (`/describe`](./describe.md))** | Automatically generating PR description - title, type, summary, code walkthrough and labels |
| **[PR Review (`/review`](./review.md))** | Adjustable feedback about the PR, possible issues, security concerns, review effort and more |
| **[Code Suggestions (`/improve`](./improve.md))** | Code suggestions for improving the PR |
| **[Question Answering (`/ask ...`](./ask.md))** | Answering free-text questions about the PR, or on specific code lines |
| **[Update Changelog (`/update_changelog`](./update_changelog.md))** | Automatically updating the CHANGELOG.md file with the PR changes |
| **[Find Similar Issue (`/similar_issue`](./similar_issues.md))** | Automatically retrieves and presents similar issues |
| **💎 [Add Documentation (`/add_docs`](./documentation.md))** | Generates documentation to methods/functions/classes that changed in the PR |
| **💎 [Generate Custom Labels (`/generate_labels`](./custom_labels.md))** | Generates custom labels for the PR, based on specific guidelines defined by the user |
| **💎 [Analyze (`/analyze`](./analyze.md))** | Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component |
| **💎 [Custom Suggestions (`/custom_suggestions`](./custom_suggestions.md))** | Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user |
| **💎 [Generate Tests (`/test component_name`](./test.md))** | Automatically generates unit tests for a selected component, based on the PR code changes |
| **💎 [CI Feedback (`/checks ci_job`](./ci_feedback.md))** | Automatically generates feedback and analysis for a failed CI job |
Note that the tools marked with 💎 are available only for PR-Agent Pro users.

168
docs/docs/tools/review.md Normal file
View File

@ -0,0 +1,168 @@
## Overview
The `review` tool scans the PR code changes, and automatically generates a PR review.
The tool can be triggered automatically every time a new PR is [opened](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened), or 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>
## Configuration options
To edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L19) related to the review tool (`pr_reviewer` section), use the following template:
```
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
```
#### General options
- `num_code_suggestions`: number of code suggestions provided by the 'review' tool. For manual comments, default is 4. For [PR-Agent app](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L142) auto tools, default is 0, meaning no code suggestions will be provided by the review tool, unless you manually edit `pr_commands`.
- `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. 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 ...".
#### 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_estimate_effort_to_review`: if set to true, the tool will add a section that estimates the effort needed to review the PR. Default is true.
#### SOC2 ticket compliance 💎
> This feature is available only in PR-Agent Pro
This sub-tool checks if the PR description properly contains a ticket to a project management system (e.g., Jira, Asana, Trello, etc.), as required by SOC2 compliance. If not, it will add a label to the PR: "Missing SOC2 ticket".
- `require_soc2_ticket`: If set to true, the SOC2 ticket checker sub-tool will be enabled. Default is false.
- `soc2_ticket_prompt`: The prompt for the SOC2 ticket review. Default is: `Does the PR description include a link to ticket in a project management system (e.g., Jira, Asana, Trello, etc.) ?`. Edit this field if your compliance requirements are different.
#### Adding PR 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 true.
#### Auto-approval
- `enable_auto_approval`: if set to true, the tool will approve the PR when invoked with the 'auto_approve' command. Default is false. This flag can be changed only from configuration file.
- `maximal_review_effort`: maximal effort level for auto-approval. If the PR's estimated review effort is above this threshold, the auto-approval will not run. Default is 5.
#### Incremental Mode
Incremental review 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.
For invoking the incremental mode, 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](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L19) 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 than 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 their 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>
## Usage Tips
### General guidelines
The `review` tool provides a collection of possible feedbacks about a PR.
It is recommended to review the [Configuration options](#configuration-options) section, and choose the relevant options for your use case.
Some of the features that are disabled by default are quite useful, and should be considered for enabling. For example:
`require_score_review`, `require_soc2_ticket`, and more.
On the other hand, if you find one of the enabled features to be irrelevant for your use case, disable it. No default configuration can fit all use cases.
### Code suggestions
If you set `num_code_suggestions`>0 , the `review` tool will also provide code suggestions.
Notice If you are interested **only** in the code suggestions, it is recommended to use the [`improve`](./improve.md) feature instead, since it is a dedicated only to code suggestions, and usually gives better results.
Use the `review` tool if you want to get more comprehensive feedback, which includes code suggestions as well.
### Automation
- When you first install the app, the [default mode](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened) for the `review` tool is:
```
pr_commands = ["/review", ...]
```
Meaning the `review` tool will run automatically on every PR, with the default configuration.
Edit this field to enable/disable the tool, or to change the used configurations.
### Auto-labels
The `review` tool can auto-generate two specific types of labels for a PR:
- a `possible security issue` label that detects a possible [security issue](https://github.com/Codium-ai/pr-agent/blob/tr/user_description/pr_agent/settings/pr_reviewer_prompts.toml#L136) (`enable_review_labels_security` flag)
- a `Review effort [1-5]: x` label, where x is the estimated effort to review the PR (`enable_review_labels_effort` flag)
Both modes are useful, and we recommended to enable them.
### Extra instructions
Extra instructions are important.
The `review` tool can be configured with extra instructions, which can be used to guide the model to a feedback tailored to the needs of your project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify the relevant sub-tool, and the relevant aspects of the PR that you want to emphasize.
Examples for extra instructions:
```
[pr_reviewer] # /review #
extra_instructions="""
In the code feedback section, emphasize the following:
- Does the code logic cover relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
### Auto-approval
PR-Agent can approve a PR when a specific comment is invoked.
To ensure safety, the auto-approval feature is disabled by default. To enable auto-approval, you need to actively set in a pre-defined configuration file the following:
```
[pr_reviewer]
enable_auto_approval = true
```
(this specific flag cannot be set with a command line argument, only in the configuration file, committed to the repository)
After enabling, by commenting on a PR:
```
/review auto_approve
```
PR-Agent will automatically approve the PR, and add a comment with the approval.
You can also enable auto-approval only if the PR meets certain requirements, such as that the `estimated_review_effort` label is equal or below a certain threshold, by adjusting the flag:
```
[pr_reviewer]
maximal_review_effort = 5
```

View File

@ -0,0 +1,61 @@
## Overview
The similar code tool retrieves the most similar code components from inside the organization's codebase, or from open-source code.
For example:
`Global Search` for a method called `chat_completion`:
<kbd><img src=https://codium.ai/images/pr_agent/similar_code_global2.png width="768"></kbd>
PR-Agent will examine the code component and will extract the most relevant keywords to search for similar code:
- `extracted keywords`: the keywords that were extracted from the code by PR-Agent. the link will open a search page with the extracted keywords, to allow the user to modify the search if needed.
- `search context`: the context in which the search will be performed, organization's codebase or open-source code (Global).
- `similar code`: the most similar code components found. the link will open the code component in the relevant file.
- `relevant repositories`: the open-source repositories in which that are relevant to the searched code component and it's keywords.
Search result link example:
<kbd><img src=https://codium.ai/images/pr_agent/code_search_result_single.png width="768"></kbd>
`Organization Search`:
<kbd><img src=https://codium.ai/images/pr_agent/similar_code_org.png width="768"></kbd>
## How to use
### Manually
To invoke the `similar code` tool manually, comment on the PR:
```
/find_similar_component COMPONENT_NAME
```
Where `COMPONENT_NAME` should be the name of a code component in the PR (class, method, function).
If there is a name ambiguity, there are two configurations that will help the tool to find the correct component:
- `--pr_find_similar_component.file`: in case there are several components with the same name, you can specify the relevant file.
- `--pr_find_similar_component.class_name`: in case there are several methods with the same name in the same file, you can specify the relevant class name.
example:
```
/find_similar_component COMPONENT_NAME --pr_find_similar_component.file=FILE_NAME
```
### Automatically (via Analyze table)
It can be invoked automatically from the analyze table, can be accessed by:
```
/analyze
```
Choose the components you want to find similar code for, and click on the `similar` checkbox.
<kbd><img src=https://codium.ai/images/pr_agent/analyze_similar.png width="768"></kbd>
If you are looking to search for similar code in the organization's codebase, you can click on the `Organization` checkbox, and it will invoke a new search command just for the organization's codebase.
<kbd><img src=https://codium.ai/images/pr_agent/similar_code_global.png width="768"></kbd>
## Configuration options
- `search_from_org`: if set to true, the tool will search for similar code in the organization's codebase. Default is false.
- `number_of_keywords`: number of keywords to use for the search. Default is 5.
- `number_of_results`: the maximum number of results to present. Default is 5.

View File

@ -1,4 +1,4 @@
# Similar Issue Tool ## Overview
The similar issue tool retrieves the most similar issues to the current issue. The similar issue tool retrieves the most similar issues to the current issue.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
@ -7,12 +7,22 @@ It can be invoked manually by commenting on any PR:
For example: 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_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_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/similar_issue.png width="768"></kbd>
Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once). Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once).
To enable usage of the '**similar issue**' tool, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
**Select VectorDBs** by changing `pr_similar_issue` parameter in `configuration.toml` file
2 VectorDBs are available to switch in
1. LanceDB
2. Pinecone
To enable usage of the '**similar issue**' tool for Pinecone, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
``` ```
[pinecone] [pinecone]
api_key = "..." api_key = "..."
@ -21,11 +31,11 @@ environment = "..."
These parameters can be obtained by registering to [Pinecone](https://app.pinecone.io/?sessionType=signup/). These parameters can be obtained by registering to [Pinecone](https://app.pinecone.io/?sessionType=signup/).
### How to use: ## How to use:
- To invoke the 'similar issue' tool from **CLI**, run: - To invoke the 'similar issue' tool from **CLI**, run:
`python3 cli.py --issue_url=... similar_issue` `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: - 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` `/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) - 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)

30
docs/docs/tools/test.md Normal file
View File

@ -0,0 +1,30 @@
## Overview
By combining LLM abilities with static code analysis, the `test` tool generate tests for a selected component, based on the PR code changes.
It can be invoked manually by commenting on any PR:
```
/test component_name
```
where 'component_name' is the name of a specific component in the PR.
To get a list of the components that changed in the PR, use the [`analyze`](./analyze.md) tool.
An example [result](https://github.com/Codium-ai/pr-agent/pull/598#issuecomment-1913679429):
<kbd><img src=https://codium.ai/images/pr_agent/test1.png width="704"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/test2.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/test3.png width="768"></kbd>
**Notes**
- Language that are currently supported by the tool: Python, Java, C++, JavaScript, TypeScript.
## Configuration options
- `num_tests`: number of tests to generate. Default is 3.
- `testing_framework`: the testing framework to use. If not set, for Python it will use `pytest`, for Java it will use `JUnit`, for C++ it will use `Catch2`, and for JavaScript and TypeScript it will use `jest`.
- `avoid_mocks`: if set to true, the tool will try to avoid using mocks in the generated tests. Note that even if this option is set to true, the tool might still use mocks if it cannot generate a test without them. Default is true.
- `extra_instructions`: Optional extra instructions to the tool. For example: "use the following mock injection scheme: ...".
- `file`: in case there are several components with the same name, you can specify the relevant file.
- `class_name`: in case there are several methods with the same name in the same file, you can specify the relevant class name.
- `enable_help_text`: if set to true, the tool will add a help text to the PR comment. Default is true.

View File

@ -1,5 +1,4 @@
# Update Changelog Tool ## Overview
The `update_changelog` tool automatically updates the CHANGELOG.md file with the PR changes. The `update_changelog` tool automatically updates the CHANGELOG.md file with the PR changes.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
@ -8,12 +7,13 @@ It can be invoked manually by commenting on any PR:
For example: 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_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/update_changelog.png width="768"></kbd> <kbd><img src=https://codium.ai/images/pr_agent/update_changelog.png width="768"></kbd>
### Configuration options ## Configuration options
Under the section 'pr_update_changelog', the [configuration file](./../pr_agent/settings/configuration.toml#L50) contains options to customize the 'update changelog' tool: Under the section 'pr_update_changelog', the [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/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). - `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 ... - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...

View File

@ -0,0 +1,218 @@
## 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](https://github.com/Codium-ai/pr-agent/blob/main/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']
```
## 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 ..."
```
## 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](../core-abilities/index.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](https://codium-ai.github.io/Docs-PR-Agent/usage-guide/#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](https://codium-ai.github.io/Docs-PR-Agent/tools/#improve) (`/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)
## Changing a model
See [here](https://github.com/Codium-ai/pr-agent/blob/main/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](https://github.com/Codium-ai/pr-agent/blob/main/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):
```
[openai]
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"
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](https://github.com/Codium-ai/pr-agent/blob/main/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.
### Anthropic
To use Anthropic models, set the relevant models in the configuration section of the configuration file:
```
[config]
model="anthropic/claude-3-opus-20240229"
model_turbo="anthropic/claude-3-opus-20240229"
fallback_models=["anthropic/claude-3-opus-20240229"]
```
And also set the api key in the .secrets.toml file:
```
[anthropic]
KEY = "..."
```
### 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.
## 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](https://github.com/Codium-ai/pr-agent/blob/main/PR_COMPRESSION.md)), PR-Agent automatically sets this number to 0, using the original git patch.
## 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](https://github.com/Codium-ai/pr-agent/blob/main/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](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/tools/pr_description.py#L137).

View File

@ -0,0 +1,200 @@
## 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: [#50](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=false
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](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) file can be similarly edited. Comment `/config` to see the list of available configurations.
## GitHub App
### GitHub app automatic tools when a new PR is opened
The [github_app](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L108) section defines GitHub app specific configurations.
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when a new PR is opened.
```
[github_app]
pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true --pr_description.final_update_message=false",
"/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
"/improve",
]
```
This means that when a new PR is opened/reopened or marked as ready for review, PR-Agent will run the `describe`, `review` and `improve` tools.
For the `describe` tool, for example, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
You can override the default tool parameters by using one the three options for a [configuration file](https://codium-ai.github.io/Docs-PR-Agent/usage-guide/#configuration-options): **wiki**, **local**, or **global**.
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 = []
```
You can also disable automatic runs for PRs with specific titles, by setting the `ignore_pr_titles` parameter with the relevant regex. For example:
```
[github_app]
ignore_pr_title = ["^[Auto]", ".*ignore.*"]
```
will ignore PRs with titles that start with "Auto" or contain the word "ignore".
### GitHub app automatic tools for push actions (commits to an open PR)
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",
"/review --pr_reviewer.num_code_suggestions=0",
]
```
This means that when new code is pushed to the PR, the PR-Agent will run the `describe` and `review` tools, with the specified parameters.
## GitHub Action
`GitHub Action` is a different way to trigger PR-Agent tools, and uses a different configuration mechanism than `GitHub App`.
You can configure settings for `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_config.auto_review: "true" # enable\disable auto review
github_action_config.auto_describe: "true" # enable\disable auto describe
github_action_config.auto_improve: "true" # enable\disable auto improve
```
`github_action_config.auto_review`, `github_action_config.auto_describe` and `github_action_config.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
If not set, the default configuration is for all three tools to 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
```
## GitLab Webhook
After setting up a GitLab webhook, to control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
```
[gitlab]
pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/review --pr_reviewer.num_code_suggestions=0",
"/improve",
]
```
## BitBucket App
Similar to GitHub app, when running PR-Agent from BitBucket App, the default [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
For example, if your local `.pr_agent.toml` file contains:
```
[pr_reviewer]
inline_code_comments = true
```
Each time you invoke a `/review` tool, it will use inline code comments.
### BitBucket Self-Hosted App automatic tools
to control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file:
Specifically, set the following values:
[bitbucket_app]
```
pr_commands = [
"/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.summarize=false",
]
```
## Azure DevOps provider
To use Azure DevOps provider use the following settings in configuration.toml:
```
[config]
git_provider="azure"
use_repo_settings_file=false
```
Azure DevOps provider supports [PAT token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows) or [DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-overview#authentication-in-server-environments) authentication.
PAT is faster to create, but has build in experation date, and will use the user identity for API calls.
Using DefaultAzureCredential you can use managed identity or Service principle, which are more secure and will create seperate ADO user identity (via AAD) to the agent.
If PAT was choosen, you can assign the value in .secrets.toml.
If DefaultAzureCredential was choosen, you can assigned the additional env vars like AZURE_CLIENT_SECRET directly,
or use managed identity/az cli (for local develpment) without any additional configuration.
in any case, 'org' value must be assigned in .secrets.toml:
```
[azure_devops]
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
# pat = "YOUR_PAT_TOKEN" needed only if using PAT for authentication
```
### Azure DevOps Webhook
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
```
[azure_devops_server]
pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/review --pr_reviewer.num_code_suggestions=0",
"/improve",
]
```

View File

@ -0,0 +1,56 @@
The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](https://github.com/Codium-ai/pr-agent/blob/main/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](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L16) section in the configuration file.
See the [Tools Guide](https://codium-ai.github.io/Docs-PR-Agent/tools/) for a detailed description of the different tools and their configurations.
There are three ways to set persistent configurations:
1. Wiki configuration page 💎
2. Local configuration file
3. Global configuration file 💎
In terms of precedence, wiki configurations will override local configurations, and local configurations will override global configurations.
## Wiki configuration file 💎
Specifically for GitHub, with PR-Agent-Pro you can set configurations by creating a page called `.pr_agent.toml` in the [wiki](https://github.com/Codium-ai/pr-agent/wiki/pr_agent.toml) of the repo.
The advantage of this method is that it allows to set configurations without needing to commit new content to the repo - just edit the wiki page and **save**.
<kbd><img src="https://codium.ai/images/pr_agent/wiki_configuration.png" width="512"></kbd>
Click [here](https://codium.ai/images/pr_agent/wiki_configuration_pr_agent.mp4) to see a short instructional video. We recommend surrounding the configuration content with triple-quotes, to allow better presentation when displayed in the wiki as markdown.
An example content:
```
[pr_description] # /describe #
keep_original_user_title=false
```
PR-Agent will know to remove the triple-quotes when reading the configuration content.
## Local configuration file
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]
extra_instructions="""\
- instruction a
- instruction b
...
"""
```
Then you can give a list of extra instructions to the `review` tool.
## Global configuration file 💎
If you create a repo called `pr-agent-settings` in your **organization**, it's configuration file `.pr_agent.toml` will be used as a global configuration file for any other repo that belongs to the same organization.
Parameters from a local `.pr_agent.toml` file, in a specific repo, will override the global configuration parameters.
For example, in the GitHub organization `Codium-ai`:
- The repo [`https://github.com/Codium-ai/pr-agent-settings`](https://github.com/Codium-ai/pr-agent-settings/blob/main/.pr_agent.toml) contains a `.pr_agent.toml` file that serves as a global configuration file for all the repos in the GitHub organization `Codium-ai`.
- The repo [`https://github.com/Codium-ai/pr-agent`](https://github.com/Codium-ai/pr-agent/blob/main/.pr_agent.toml) inherits the global configuration file from `pr-agent-settings`.

View File

@ -0,0 +1,20 @@
# Usage guide
- [Introduction](./introduction.md)
- [Configuration Options](./configuration_options.md)
- [Managing Mail Notifications](./mail_notifications.md)
- [Usage and Automation](./automations_and_usage.md)
- [Local Repo (CLI)](./automations_and_usage.md#local-repo-cli)
- [Online Usage](./automations_and_usage.md#online-usage)
- [GitHub App](./automations_and_usage.md#github-app)
- [GitHub Action](./automations_and_usage.md#github-action)
- [GitLab Webhook](./automations_and_usage.md#gitlab-webhook)
- [BitBucket App](./automations_and_usage.md#bitbucket-app)
- [Azure DevOps Provider](./automations_and_usage.md#azure-devops-provider)
- [Additional Configurations Walkthrough](./additional_configurations.md)
- [Ignoring files from analysis](./additional_configurations.md#ignoring-files-from-analysis)
- [Extra instructions](./additional_configurations.md#extra-instructions)
- [Working with large PRs](./additional_configurations.md#working-with-large-prs)
- [Changing a model](./additional_configurations.md#changing-a-model)
- [Patch Extra Lines](./additional_configurations.md#patch-extra-lines)
- [Editing the prompts](./additional_configurations.md#editing-the-prompts)

View File

@ -0,0 +1,18 @@
After [installation](https://codium-ai.github.io/Docs-PR-Agent/installation/), 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](https://codium-ai.github.io/Docs-PR-Agent/installation/#run-from-source), or by invoking a [locally cloned repo](https://codium-ai.github.io/Docs-PR-Agent/installation/#locally).
For online usage, you will need to setup either a [GitHub App](https://codium-ai.github.io/Docs-PR-Agent/installation/#run-as-a-github-app), or a [GitHub Action](https://codium-ai.github.io/Docs-PR-Agent/installation/#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.
**git provider**: The [git_provider](https://github.com/Codium-ai/pr-agent/blob/main/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", "bitbucket", "azure", "codecommit", "local", "gerrit"
`

View File

@ -0,0 +1,9 @@
Unfortunately, it is not possible in GitHub to disable mail notifications from a specific user.
If you are subscribed to notifications for a repo with PR-Agent, we recommend turning off notifications for PR comments, to avoid lengthy emails:
<kbd><img src="https://codium.ai/images/pr_agent/notifications.png" width="512"></kbd>
As an alternative, you can filter in your mail provider the notifications specifically from the PR-Agent bot, [see how](https://www.quora.com/How-can-you-filter-emails-for-specific-people-in-Gmail#:~:text=On%20the%20Filters%20and%20Blocked,the%20body%20of%20the%20email).
<kbd><img src="https://codium.ai/images/pr_agent/filter_mail_notifications.png" width="512"></kbd>

121
docs/mkdocs.yml Normal file
View File

@ -0,0 +1,121 @@
site_name: PR-Agent
nav:
- Overview: 'index.md'
- Installation:
- 'installation/index.md'
- Locally: 'installation/locally.md'
- GitHub: 'installation/github.md'
- GitLab: 'installation/gitlab.md'
- BitBucket: 'installation/bitbucket.md'
- Azure DevOps: 'installation/azure.md'
- Usage Guide:
- 'usage-guide/index.md'
- Introduction: 'usage-guide/introduction.md'
- Configuration Options: 'usage-guide/configuration_options.md'
- Managing email notifications: 'usage-guide/mail_notifications.md'
- Usage and Automation: 'usage-guide/automations_and_usage.md'
- Additional Configurations: 'usage-guide/additional_configurations.md'
- Tools:
- 'tools/index.md'
- Describe: 'tools/describe.md'
- Review: 'tools/review.md'
- Improve: 'tools/improve.md'
- Ask: 'tools/ask.md'
- Update Changelog: 'tools/update_changelog.md'
- Similar Issues: 'tools/similar_issues.md'
- 💎 Analyze: 'tools/analyze.md'
- 💎 Test: 'tools/test.md'
- 💎 Documentation: 'tools/documentation.md'
- 💎 Custom Labels: 'tools/custom_labels.md'
- 💎 Custom Suggestions: 'tools/custom_suggestions.md'
- 💎 CI Feedback: 'tools/ci_feedback.md'
- 💎 Similar Code: 'tools/similar_code.md'
- Core Abilities: 'core-abilities/index.md'
theme:
logo: assets/logo.png
favicon: assets/favicon.ico
name: material
features:
- navigation.tabs
- navigation.expand
- navigation.path
- navigation.top
- navigation.tracking
- navigation.indexes
- search.suggest
- search.highlight
- content.tabs.link
- content.code.annotation
- content.code.copy
- toc.integrate
language: en
custom_dir: overrides
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
primary: custom
accent: custom
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/toggle-switch
name: Switch to light mode
primary: custom
accent: custom
plugins:
- social
- search
extra:
generator: false
social:
- icon: fontawesome/brands/github
link: https://github.com/Codium-ai
- icon: fontawesome/brands/discord
link: https://discord.com/invite/SgSxuQ65GF
- icon: fontawesome/brands/youtube
link: https://www.youtube.com/@Codium-AI
- icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/company/codiumai
- icon: fontawesome/brands/twitter
link: https://twitter.com/CodiumAI
- icon: fontawesome/brands/instagram
link: https://www.instagram.com/codiumai/
extra_css:
- css/custom.css
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.arithmatex:
generic: true
- footnotes
- pymdownx.details
- pymdownx.superfences
- pymdownx.mark
- attr_list
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- toc:
title: On this page
toc_depth: 3
permalink: true
copyright: |
&copy; 2024 <a href="https://www.codium.ai/" target="_blank" rel="noopener">CodiumAI</a>

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Footer Customization</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
}
.wrapper {
background-color: #171518;
}
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: white;
padding: 20px;
max-width: 61rem;
margin-left: auto;
margin-right: auto;
}
.footer-links {
display: flex;
justify-content: center;
gap: 20px;
margin: 10px 0;
}
.footer-links, .social-icons {
padding: 0;
list-style-type: none;
display: flex;
justify-content: center;
gap: 20px;
align-items: center;
}
.footer-links a:hover {
color: #AEA1F1;
}
.social-icons {
display: flex;
gap: 20px;
align-items: center;
}
.social-icons a:hover {
color: #AEA1F1;
}
.social-icons svg {
width: 24px;
height: auto;
fill: white;
}
.footer-text {
width: 240px;
}
</style>
</head>
<body>
<footer class="wrapper">
<div class="container">
<p class="footer-text">© 2024 <a href="https://www.codium.ai/" target="_blank" rel="noopener">CodiumAI</a></p>
<div class="footer-links">
<a href="https://codiumate-docs.codium.ai/">Codiumate</a>
<p>|</p>
<a href="https://alpha-codium-docs.codium.ai/">AlphaCodium</a>
</div>
<div class="social-icons">
<a href="https://github.com/Codium-ai" target="_blank" rel="noopener" title="github.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
</a>
<a href="https://discord.com/invite/SgSxuQ65GF" target="_blank" rel="noopener" title="discord.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M524.531 69.836a1.5 1.5 0 0 0-.764-.7A485.065 485.065 0 0 0 404.081 32.03a1.816 1.816 0 0 0-1.923.91 337.461 337.461 0 0 0-14.9 30.6 447.848 447.848 0 0 0-134.426 0 309.541 309.541 0 0 0-15.135-30.6 1.89 1.89 0 0 0-1.924-.91 483.689 483.689 0 0 0-119.688 37.107 1.712 1.712 0 0 0-.788.676C39.068 183.651 18.186 294.69 28.43 404.354a2.016 2.016 0 0 0 .765 1.375 487.666 487.666 0 0 0 146.825 74.189 1.9 1.9 0 0 0 2.063-.676A348.2 348.2 0 0 0 208.12 430.4a1.86 1.86 0 0 0-1.019-2.588 321.173 321.173 0 0 1-45.868-21.853 1.885 1.885 0 0 1-.185-3.126 251.047 251.047 0 0 0 9.109-7.137 1.819 1.819 0 0 1 1.9-.256c96.229 43.917 200.41 43.917 295.5 0a1.812 1.812 0 0 1 1.924.233 234.533 234.533 0 0 0 9.132 7.16 1.884 1.884 0 0 1-.162 3.126 301.407 301.407 0 0 1-45.89 21.83 1.875 1.875 0 0 0-1 2.611 391.055 391.055 0 0 0 30.014 48.815 1.864 1.864 0 0 0 2.063.7A486.048 486.048 0 0 0 610.7 405.729a1.882 1.882 0 0 0 .765-1.352c12.264-126.783-20.532-236.912-86.934-334.541ZM222.491 337.58c-28.972 0-52.844-26.587-52.844-59.239s23.409-59.241 52.844-59.241c29.665 0 53.306 26.82 52.843 59.239 0 32.654-23.41 59.241-52.843 59.241Zm195.38 0c-28.971 0-52.843-26.587-52.843-59.239s23.409-59.241 52.843-59.241c29.667 0 53.307 26.82 52.844 59.239 0 32.654-23.177 59.241-52.844 59.241Z"></path></svg>
</a>
<a href="https://www.youtube.com/@Codium-AI" target="_blank" rel="noopener" title="www.youtube.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"></path></svg>
</a>
<a href="https://www.linkedin.com/company/codiumai" target="_blank" rel="noopener" title="www.linkedin.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"></path></svg>
</a>
<a href="https://twitter.com/CodiumAI" target="_blank" rel="noopener" title="twitter.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
</a>
<a href="https://www.instagram.com/codiumai/" target="_blank" rel="noopener" title="www.instagram.com" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"></path></svg>
</a>
</div>
</div>
</footer>
</body>
</html>

View File

@ -1,14 +1,21 @@
import shlex import shlex
from functools import partial
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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.utils import apply_repo_settings from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import get_logger
from pr_agent.tools.pr_add_docs import PRAddDocs 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_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_generate_labels import PRGenerateLabels
from pr_agent.tools.pr_help_message import PRHelpMessage
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_line_questions import PR_LineQuestions
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_similar_issue import PRSimilarIssue
@ -27,9 +34,11 @@ command2class = {
"improve_code": PRCodeSuggestions, "improve_code": PRCodeSuggestions,
"ask": PRQuestions, "ask": PRQuestions,
"ask_question": PRQuestions, "ask_question": PRQuestions,
"ask_line": PR_LineQuestions,
"update_changelog": PRUpdateChangelog, "update_changelog": PRUpdateChangelog,
"config": PRConfig, "config": PRConfig,
"settings": PRConfig, "settings": PRConfig,
"help": PRHelpMessage,
"similar_issue": PRSimilarIssue, "similar_issue": PRSimilarIssue,
"add_docs": PRAddDocs, "add_docs": PRAddDocs,
"generate_labels": PRGenerateLabels, "generate_labels": PRGenerateLabels,
@ -38,8 +47,9 @@ command2class = {
commands = list(command2class.keys()) commands = list(command2class.keys())
class PRAgent: class PRAgent:
def __init__(self): def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
pass self.ai_handler = ai_handler # will be initialized in run_action
self.forbidden_cli_args = ['enable_auto_approval']
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
@ -53,22 +63,32 @@ class PRAgent:
action, *args = list(lexer) action, *args = list(lexer)
else: else:
action, *args = request action, *args = request
if args:
for forbidden_arg in self.forbidden_cli_args:
for arg in args:
if forbidden_arg in arg:
get_logger().error(f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file.")
return False
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": with get_logger().contextualize(command=action):
get_settings().pr_reviewer.ask_and_reflect = True get_logger().info("PR-Agent request handler started", analytics=True)
if action == "answer": if action == "reflect_and_review":
if notify: get_settings().pr_reviewer.ask_and_reflect = True
notify() if action == "answer":
await PRReviewer(pr_url, is_answer=True, args=args).run() if notify:
elif action == "auto_review": notify()
await PRReviewer(pr_url, is_auto=True, args=args).run() await PRReviewer(pr_url, is_answer=True, args=args, ai_handler=self.ai_handler).run()
elif action in command2class: elif action == "auto_review":
if notify: await PRReviewer(pr_url, is_auto=True, args=args, ai_handler=self.ai_handler).run()
notify() elif action in command2class:
await command2class[action](pr_url, args=args).run() if notify:
else: notify()
return False
return True await command2class[action](pr_url, ai_handler=self.ai_handler, args=args).run()
else:
return False
return True

View File

@ -9,6 +9,7 @@ MAX_TOKENS = {
'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 'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4-0125-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,
@ -18,7 +19,8 @@ MAX_TOKENS = {
'vertex_ai/codechat-bison-32k': 32000, 'vertex_ai/codechat-bison-32k': 32000,
'codechat-bison': 6144, 'codechat-bison': 6144,
'codechat-bison-32k': 32000, 'codechat-bison-32k': 32000,
'anthropic.claude-v2': 100000,
'anthropic.claude-instant-v1': 100000, 'anthropic.claude-instant-v1': 100000,
'anthropic.claude-v1': 100000, 'anthropic.claude-v1': 100000,
'anthropic.claude-v2': 100000,
'anthropic/claude-3-opus-20240229': 100000,
} }

View File

@ -0,0 +1,28 @@
from abc import ABC, abstractmethod
class BaseAiHandler(ABC):
"""
This class defines the interface for an AI handler to be used by the PR Agents.
"""
@abstractmethod
def __init__(self):
pass
@property
@abstractmethod
def deployment_id(self):
pass
@abstractmethod
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
"""
This method should be implemented to return a chat completion from the AI model.
Args:
model (str): the name of the model to use for the chat completion
system (str): the system message string to use for the chat completion
user (str): the user message string to use for the chat completion
temperature (float): the temperature to use for the chat completion
"""
pass

View File

@ -0,0 +1,67 @@
try:
from langchain.chat_models import ChatOpenAI, AzureChatOpenAI
from langchain.schema import SystemMessage, HumanMessage
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
pass
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry
import functools
OPENAI_RETRIES = 5
class LangChainOpenAIHandler(BaseAiHandler):
def __init__(self):
# Initialize OpenAIHandler specific attributes here
super().__init__()
self.azure = get_settings().get("OPENAI.API_TYPE", "").lower() == "azure"
try:
if self.azure:
# using a partial function so we can set the deployment_id later to support fallback_deployments
# but still need to access the other settings now so we can raise a proper exception if they're missing
self._chat = functools.partial(
lambda **kwargs: AzureChatOpenAI(**kwargs),
openai_api_key=get_settings().openai.key,
openai_api_base=get_settings().openai.api_base,
openai_api_version=get_settings().openai.api_version,
)
else:
self._chat = ChatOpenAI(openai_api_key=get_settings().openai.key)
except AttributeError as e:
if getattr(e, "name"):
raise ValueError(f"OpenAI {e.name} is required") from e
else:
raise e
@property
def chat(self):
if self.azure:
# we must set the deployment_id only here (instead of the __init__ method) to support fallback_deployments
return self._chat(deployment_name=self.deployment_id)
else:
return self._chat
@property
def deployment_id(self):
"""
Returns the deployment ID for the OpenAI API.
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try:
messages=[SystemMessage(content=system), HumanMessage(content=user)]
# get a chat completion from the formatted messages
resp = self.chat(messages, model=model, temperature=temperature)
finish_reason="completed"
return resp.content, finish_reason
except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e)
raise e

View File

@ -4,15 +4,15 @@ 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 tenacity import retry, retry_if_exception_type, stop_after_attempt
from retry import retry from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
OPENAI_RETRIES = 5 OPENAI_RETRIES = 5
class AiHandler: class LiteLLMAIHandler(BaseAiHandler):
""" """
This class handles interactions with the OpenAI API for chat completions. This class handles interactions with the OpenAI API for chat completions.
It initializes the API key and other settings from a configuration file, It initializes the API key and other settings from a configuration file,
@ -26,7 +26,8 @@ class AiHandler:
""" """
self.azure = False self.azure = False
self.aws_bedrock_client = None self.aws_bedrock_client = None
self.api_base = None
self.repetition_penalty = None
if get_settings().get("OPENAI.KEY", None): if get_settings().get("OPENAI.KEY", None):
openai.api_key = get_settings().openai.key openai.api_key = get_settings().openai.key
litellm.openai_key = get_settings().openai.key litellm.openai_key = get_settings().openai.key
@ -55,8 +56,11 @@ 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
if get_settings().get("HUGGINGFACE.API_BASE", None): if get_settings().get("HUGGINGFACE.API_BASE", None) and 'huggingface' in get_settings().config.model:
litellm.api_base = get_settings().huggingface.api_base litellm.api_base = get_settings().huggingface.api_base
self.api_base = get_settings().huggingface.api_base
if get_settings().get("HUGGINGFACE.REPITITION_PENALTY", None):
self.repetition_penalty = float(get_settings().huggingface.repetition_penalty)
if get_settings().get("VERTEXAI.VERTEX_PROJECT", None): if get_settings().get("VERTEXAI.VERTEX_PROJECT", None):
litellm.vertex_project = get_settings().vertexai.vertex_project litellm.vertex_project = get_settings().vertexai.vertex_project
litellm.vertex_location = get_settings().get( litellm.vertex_location = get_settings().get(
@ -76,35 +80,14 @@ class AiHandler:
""" """
return get_settings().get("OPENAI.DEPLOYMENT_ID", None) return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError), @retry(
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3)) retry=retry_if_exception_type((openai.APIError, openai.APIConnectionError, openai.Timeout)), # No retry on RateLimitError
stop=stop_after_attempt(OPENAI_RETRIES)
)
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2): async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
"""
Performs a chat completion using the OpenAI ChatCompletion API.
Retries in case of API errors or timeouts.
Args:
model (str): The model to use for chat completion.
temperature (float): The temperature parameter for chat completion.
system (str): The system message for chat completion.
user (str): The user message for chat completion.
Returns:
tuple: A tuple containing the response and finish reason from the API.
Raises:
TryAgain: If the API response is empty or there are no choices in the response.
APIError: If there is an error during OpenAI inference.
Timeout: If there is a timeout during OpenAI inference.
TryAgain: If there is an attribute error during OpenAI inference.
"""
try: try:
resp, finish_reason = None, None
deployment_id = self.deployment_id deployment_id = self.deployment_id
if get_settings().config.verbosity_level >= 2:
get_logger().debug(
f"Generating completion with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
)
if self.azure: if self.azure:
model = 'azure/' + model model = 'azure/' + model
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
@ -114,24 +97,39 @@ class AiHandler:
"messages": messages, "messages": messages,
"temperature": temperature, "temperature": temperature,
"force_timeout": get_settings().config.ai_timeout, "force_timeout": get_settings().config.ai_timeout,
"api_base" : self.api_base,
} }
if self.aws_bedrock_client: if self.aws_bedrock_client:
kwargs["aws_bedrock_client"] = self.aws_bedrock_client kwargs["aws_bedrock_client"] = self.aws_bedrock_client
if self.repetition_penalty:
kwargs["repetition_penalty"] = self.repetition_penalty
get_logger().debug("Prompts", artifact={"system": system, "user": user})
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nSystem prompt:\n{system}")
get_logger().info(f"\nUser prompt:\n{user}")
response = await acompletion(**kwargs) response = await acompletion(**kwargs)
except (APIError, Timeout, TryAgain) as e: except (openai.APIError, openai.Timeout) as e:
get_logger().error("Error during OpenAI inference: ", e) get_logger().error("Error during OpenAI inference: ", e)
raise raise
except (RateLimitError) as e: except (openai.RateLimitError) as e:
get_logger().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:
get_logger().error("Unknown error during OpenAI inference: ", e) get_logger().error("Unknown error during OpenAI inference: ", e)
raise TryAgain from e raise openai.APIError from e
if response is None or len(response["choices"]) == 0: if response is None or len(response["choices"]) == 0:
raise TryAgain raise openai.APIError
resp = response["choices"][0]['message']['content'] else:
finish_reason = response["choices"][0]["finish_reason"] resp = response["choices"][0]['message']['content']
usage = response.get("usage") finish_reason = response["choices"][0]["finish_reason"]
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason, # usage = response.get("usage")
model=model, usage=usage) get_logger().debug(f"\nAI response:\n{resp}")
return resp, finish_reason get_logger().debug("Full_response", artifact=response)
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nAI response:\n{resp}")
return resp, finish_reason

View File

@ -0,0 +1,67 @@
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
import openai
from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
OPENAI_RETRIES = 5
class OpenAIHandler(BaseAiHandler):
def __init__(self):
# Initialize OpenAIHandler specific attributes here
try:
super().__init__()
openai.api_key = get_settings().openai.key
if get_settings().get("OPENAI.ORG", None):
openai.organization = get_settings().openai.org
if get_settings().get("OPENAI.API_TYPE", None):
if get_settings().openai.api_type == "azure":
self.azure = True
openai.azure_key = get_settings().openai.key
if get_settings().get("OPENAI.API_VERSION", None):
openai.api_version = get_settings().openai.api_version
if get_settings().get("OPENAI.API_BASE", None):
openai.api_base = get_settings().openai.api_base
except AttributeError as e:
raise ValueError("OpenAI key is required") from e
@property
def deployment_id(self):
"""
Returns the deployment ID for the OpenAI API.
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try:
deployment_id = self.deployment_id
get_logger().info("System: ", system)
get_logger().info("User: ", user)
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
chat_completion = await openai.ChatCompletion.acreate(
model=model,
deployment_id=deployment_id,
messages=messages,
temperature=temperature,
)
resp = chat_completion["choices"][0]['message']['content']
finish_reason = chat_completion["choices"][0]["finish_reason"]
usage = chat_completion.get("usage")
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage)
return resp, finish_reason
except (APIError, Timeout, TryAgain) as e:
get_logger().error("Error during OpenAI inference: ", e)
raise
except (RateLimitError) as e:
get_logger().error("Rate limit error during OpenAI inference: ", e)
raise
except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e)
raise TryAgain from e

View File

@ -3,7 +3,7 @@ from __future__ import annotations
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.algo.types import EDIT_TYPE, FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -181,7 +181,7 @@ __old hunk__
... ...
""" """
patch_with_lines_str = f"\n\n## {file.filename}\n" patch_with_lines_str = f"\n\n## file: '{file.filename.strip()}'\n"
patch_lines = patch.splitlines() patch_lines = patch.splitlines()
RE_HUNK_HEADER = re.compile( RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
@ -202,11 +202,11 @@ __old hunk__
if new_content_lines: if new_content_lines:
if prev_header_line: if prev_header_line:
patch_with_lines_str += f'\n{prev_header_line}\n' patch_with_lines_str += f'\n{prev_header_line}\n'
patch_with_lines_str += '__new hunk__\n' patch_with_lines_str = patch_with_lines_str.rstrip()+'\n__new hunk__\n'
for i, line_new in enumerate(new_content_lines): for i, line_new in enumerate(new_content_lines):
patch_with_lines_str += f"{start2 + i} {line_new}\n" patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines: if old_content_lines:
patch_with_lines_str += '__old hunk__\n' patch_with_lines_str = patch_with_lines_str.rstrip()+'\n__old hunk__\n'
for line_old in old_content_lines: for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str += f"{line_old}\n"
new_content_lines = [] new_content_lines = []
@ -236,12 +236,68 @@ __old hunk__
if match and new_content_lines: if match and new_content_lines:
if new_content_lines: if new_content_lines:
patch_with_lines_str += f'\n{header_line}\n' patch_with_lines_str += f'\n{header_line}\n'
patch_with_lines_str += '\n__new hunk__\n' patch_with_lines_str = patch_with_lines_str.rstrip()+ '\n__new hunk__\n'
for i, line_new in enumerate(new_content_lines): for i, line_new in enumerate(new_content_lines):
patch_with_lines_str += f"{start2 + i} {line_new}\n" patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines: if old_content_lines:
patch_with_lines_str += '\n__old hunk__\n' patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
for line_old in old_content_lines: for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str += f"{line_old}\n"
return patch_with_lines_str.rstrip() return patch_with_lines_str.rstrip()
def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, side) -> tuple[str, str]:
patch_with_lines_str = f"\n\n## file: '{file_name.strip()}'\n\n"
selected_lines = ""
patch_lines = patch.splitlines()
RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
match = None
start1, size1, start2, size2 = -1, -1, -1, -1
skip_hunk = False
selected_lines_num = 0
for line in patch_lines:
if 'no newline at end of file' in line.lower():
continue
if line.startswith('@@'):
skip_hunk = False
selected_lines_num = 0
header_line = line
match = RE_HUNK_HEADER.match(line)
res = list(match.groups())
for i in range(len(res)):
if res[i] is None:
res[i] = 0
try:
start1, size1, start2, size2 = map(int, res[:4])
except: # '@@ -0,0 +1 @@' case
start1, size1, size2 = map(int, res[:3])
start2 = 0
# check if line range is in this hunk
if side.lower() == 'left':
# check if line range is in this hunk
if not (start1 <= line_start <= start1 + size1):
skip_hunk = True
continue
elif side.lower() == 'right':
if not (start2 <= line_start <= start2 + size2):
skip_hunk = True
continue
patch_with_lines_str += f'\n{header_line}\n'
elif not skip_hunk:
if side.lower() == 'right' and line_start <= start2 + selected_lines_num <= line_end:
selected_lines += line + '\n'
if side.lower() == 'left' and start1 <= selected_lines_num + start1 <= line_end:
selected_lines += line + '\n'
patch_with_lines_str += line + '\n'
if not line.startswith('-'): # currently we don't support /ask line for deleted lines
selected_lines_num += 1
return patch_with_lines_str.rstrip(), selected_lines.rstrip()

View File

@ -1,9 +1,7 @@
from __future__ import annotations from __future__ import annotations
import difflib
import re
import traceback import traceback
from typing import Any, Callable, List, Tuple from typing import Callable, List, Tuple
from github import RateLimitExceededException from github import RateLimitExceededException
@ -11,9 +9,10 @@ from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbe
from pr_agent.algo.language_handler import sort_files_by_main_languages from pr_agent.algo.language_handler import sort_files_by_main_languages
from pr_agent.algo.file_filter import filter_ignored from pr_agent.algo.file_filter import filter_ignored
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import get_max_tokens from pr_agent.algo.utils import get_max_tokens, ModelType
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from pr_agent.git_providers.git_provider import GitProvider
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
DELETED_FILES_ = "Deleted files:\n" DELETED_FILES_ = "Deleted files:\n"
@ -51,15 +50,29 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines
try: try:
diff_files = git_provider.get_diff_files() diff_files_original = git_provider.get_diff_files()
except RateLimitExceededException as e: except RateLimitExceededException as e:
get_logger().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) diff_files = filter_ignored(diff_files_original)
if diff_files != diff_files_original:
try:
get_logger().info(f"Filtered out {len(diff_files_original) - len(diff_files)} files")
new_names = set([a.filename for a in diff_files])
orig_names = set([a.filename for a in diff_files_original])
get_logger().info(f"Filtered out files: {orig_names - new_names}")
except Exception as e:
pass
# 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)
if pr_languages:
try:
get_logger().info(f"PR main language: {pr_languages[0]['language']}")
except Exception as e:
pass
# 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( patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(
@ -67,9 +80,13 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
# 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 < get_max_tokens(model): if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < get_max_tokens(model):
get_logger().info(f"Tokens: {total_tokens}, total tokens under limit: {get_max_tokens(model)}, "
f"returning full diff.")
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
get_logger().info(f"Tokens: {total_tokens}, total tokens over limit: {get_max_tokens(model)}, "
f"pruning diff.")
patches_compressed, modified_file_names, deleted_file_names, added_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)
@ -83,6 +100,11 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
if deleted_file_names: if deleted_file_names:
deleted_list_str = DELETED_FILES_ + "\n".join(deleted_file_names) deleted_list_str = DELETED_FILES_ + "\n".join(deleted_file_names)
final_diff = final_diff + "\n\n" + deleted_list_str final_diff = final_diff + "\n\n" + deleted_list_str
try:
get_logger().debug(f"After pruning, added_list_str: {added_list_str}, modified_list_str: {modified_list_str}, "
f"deleted_list_str: {deleted_list_str}")
except Exception as e:
pass
return final_diff return final_diff
@ -209,9 +231,9 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
if patch: if patch:
if not convert_hunks_to_line_numbers: if not convert_hunks_to_line_numbers:
patch_final = f"## {file.filename}\n\n{patch}\n" patch_final = f"\n\n## file: '{file.filename.strip()}\n\n{patch.strip()}\n'"
else: else:
patch_final = patch patch_final = "\n\n" + patch.strip()
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:
@ -220,15 +242,19 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
return patches, modified_files_list, deleted_files_list, added_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, model_type: ModelType = ModelType.REGULAR):
all_models = _get_all_models() all_models = _get_all_models(model_type)
all_deployments = _get_all_deployments(all_models) all_deployments = _get_all_deployments(all_models)
# try each (model, deployment_id) pair until one is successful, otherwise raise exception # try each (model, deployment_id) pair until one is successful, otherwise raise exception
for i, (model, deployment_id) in enumerate(zip(all_models, all_deployments)): for i, (model, deployment_id) in enumerate(zip(all_models, all_deployments)):
try: try:
get_logger().debug(
f"Generating prediction with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
)
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:
get_logger().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 ''}: "
@ -238,8 +264,11 @@ async def retry_with_fallback_models(f: Callable):
raise # Re-raise the last exception raise # Re-raise the last exception
def _get_all_models() -> List[str]: def _get_all_models(model_type: ModelType = ModelType.REGULAR) -> List[str]:
model = get_settings().config.model if model_type == ModelType.TURBO:
model = get_settings().config.model_turbo
else:
model = get_settings().config.model
fallback_models = get_settings().config.fallback_models fallback_models = get_settings().config.fallback_models
if not isinstance(fallback_models, list): if not isinstance(fallback_models, list):
fallback_models = [m.strip() for m in fallback_models.split(",")] fallback_models = [m.strip() for m in fallback_models.split(",")]
@ -262,70 +291,6 @@ def _get_all_deployments(all_models: List[str]) -> List[str]:
return all_deployments return all_deployments
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
relevant_file: str,
relevant_line_in_file: str) -> Tuple[int, int]:
"""
Find the line number and absolute position of a relevant line in a file.
Args:
diff_files (List[FilePatchInfo]): A list of FilePatchInfo objects representing the patches of files.
relevant_file (str): The name of the file where the relevant line is located.
relevant_line_in_file (str): The content of the relevant line.
Returns:
Tuple[int, int]: A tuple containing the line number and absolute position of the relevant line in the file.
"""
position = -1
absolute_position = -1
re_hunk_header = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
for file in diff_files:
if file.filename and (file.filename.strip() == relevant_file):
patch = file.patch
patch_lines = patch.splitlines()
# try to find the line in the patch using difflib, with some margin of error
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
patch_lines, n=3, cutoff=0.93)
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
relevant_line_in_file = matches_difflib[0]
delta = 0
start1, size1, start2, size2 = 0, 0, 0, 0
for i, line in enumerate(patch_lines):
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
if relevant_line_in_file in line and line[0] != '-':
position = i
absolute_position = start2 + delta - 1
break
if position == -1 and relevant_line_in_file[0] == '+':
no_plus_line = relevant_line_in_file[1:].lstrip()
for i, line in enumerate(patch_lines):
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
if no_plus_line in line and line[0] != '-':
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
position = i
absolute_position = start2 + delta - 1
break
return position, absolute_position
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,
@ -362,6 +327,13 @@ def get_pr_multi_diffs(git_provider: GitProvider,
for lang in pr_languages: for lang in pr_languages:
sorted_files.extend(sorted(lang['files'], key=lambda x: x.tokens, reverse=True)) sorted_files.extend(sorted(lang['files'], key=lambda x: x.tokens, reverse=True))
# try first a single run with standard diff string, with patch extension, and no deletions
patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(
pr_languages, token_handler, add_line_numbers_to_hunks=True)
if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < get_max_tokens(model):
return ["\n".join(patches_extended)]
patches = [] patches = []
final_diff_list = [] final_diff_list = []
total_tokens = token_handler.prompt_tokens total_tokens = token_handler.prompt_tokens
@ -385,6 +357,11 @@ def get_pr_multi_diffs(git_provider: GitProvider,
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 (token_handler.prompt_tokens + new_patch_tokens) > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
get_logger().warning(f"Patch too large, skipping: {file.filename}")
continue
if patch and (total_tokens + new_patch_tokens > get_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)

23
pr_agent/algo/types.py Normal file
View File

@ -0,0 +1,23 @@
from dataclasses import dataclass
from enum import Enum
class EDIT_TYPE(Enum):
ADDED = 1
DELETED = 2
MODIFIED = 3
RENAMED = 4
UNKNOWN = 5
@dataclass
class FilePatchInfo:
base_file: str
head_file: str
patch: str
filename: str
tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
old_filename: str = None
num_plus_lines: int = -1
num_minus_lines: int = -1

View File

@ -5,7 +5,8 @@ import json
import re import re
import textwrap import textwrap
from datetime import datetime from datetime import datetime
from typing import Any, List from enum import Enum
from typing import Any, List, Tuple
import yaml import yaml
from starlette_context import context from starlette_context import context
@ -13,8 +14,12 @@ from starlette_context import context
from pr_agent.algo import MAX_TOKENS from pr_agent.algo import MAX_TOKENS
from pr_agent.algo.token_handler import get_token_encoder from pr_agent.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.algo.types import FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
class ModelType(str, Enum):
REGULAR = "regular"
TURBO = "turbo"
def get_setting(key: str) -> Any: def get_setting(key: str) -> Any:
try: try:
@ -23,7 +28,39 @@ 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, gfm_supported: bool=True) -> str:
def emphasize_header(text: str) -> str:
try:
# Finding the position of the first occurrence of ": "
colon_position = text.find(": ")
# Splitting the string and wrapping the first part in <strong> tags
if colon_position != -1:
# Everything before the colon (inclusive) is wrapped in <strong> tags
transformed_string = "<strong>" + text[:colon_position + 1] + "</strong>" + text[colon_position + 1:]
else:
# If there's no ": ", return the original string
transformed_string = text
return transformed_string
except Exception as e:
get_logger().exception(f"Failed to emphasize header: {e}")
return text
def unique_strings(input_list: List[str]) -> List[str]:
if not input_list or not isinstance(input_list, list):
return input_list
seen = set()
unique_list = []
for item in input_list:
if item not in seen:
unique_list.append(item)
seen.add(item)
return unique_list
def convert_to_markdown(output_data: dict, gfm_supported: bool = True, incremental_review=None) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
Args: Args:
@ -31,84 +68,151 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
Returns: Returns:
str: The markdown formatted text generated from the input dictionary. str: The markdown formatted text generated from the input dictionary.
""" """
markdown_text = ""
emojis = { emojis = {
"Main theme": "🎯", "Possible issues": "🔍",
"PR summary": "📝",
"Type of PR": "📌",
"Score": "🏅", "Score": "🏅",
"Relevant tests added": "🧪", "Relevant tests": "🧪",
"Unrelated changes": "⚠️",
"Focused PR": "", "Focused PR": "",
"Security concerns": "🔒", "Security concerns": "🔒",
"General suggestions": "💡",
"Insights from user's answers": "📝", "Insights from user's answers": "📝",
"Code feedback": "🤖", "Code feedback": "🤖",
"Estimated effort to review [1-5]": "⏱️", "Estimated effort to review [1-5]": "⏱️",
} }
markdown_text = ""
if not incremental_review:
markdown_text += f"## PR Review\n\n"
else:
markdown_text += f"## Incremental PR Review\n\n"
markdown_text += f"⏮️ Review for commits since previous PR-Agent review {incremental_review}.\n\n"
if gfm_supported:
markdown_text += "<table>\n<tr>\n"
# markdown_text += """<td> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Feedback&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td> <td></td></tr>"""
for key, value in output_data.items(): if not output_data or not output_data.get('review', {}):
if value is None or value == '' or value == {}: return ""
for key, value in output_data['review'].items():
if value is None or value == '' or value == {} or value == []:
continue continue
if isinstance(value, dict): key_nice = key.replace('_', ' ').capitalize()
markdown_text += f"## {key}\n\n" emoji = emojis.get(key_nice, "")
markdown_text += convert_to_markdown(value, gfm_supported) if gfm_supported:
elif isinstance(value, list): if 'Estimated effort to review' in key_nice:
emoji = emojis.get(key, "") key_nice = 'Estimated&nbsp;effort&nbsp;to&nbsp;review [1-5]'
if key.lower() == 'code feedback': if 'security concerns' in key_nice.lower():
if gfm_supported: value = emphasize_header(value.strip())
markdown_text += f"\n\n- " markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n"
markdown_text += f"<details><summary> { emoji } Code feedback:</summary>\n\n" elif 'possible issues' in key_nice.lower():
value = value.strip()
issues = value.split('\n- ')
for i, _ in enumerate(issues):
issues[i] = issues[i].strip().strip('-').strip()
issues = unique_strings(issues) # remove duplicates
number_of_issues = len(issues)
if number_of_issues > 1:
markdown_text += f"<tr><td rowspan={number_of_issues}> {emoji}&nbsp;<strong>{key_nice}</strong></td>\n"
for i, issue in enumerate(issues):
if not issue:
continue
issue = emphasize_header(issue)
if i == 0:
markdown_text += f"<td>\n\n{issue}</td></tr>\n"
else:
markdown_text += f"<tr>\n<td>\n\n{issue}</td></tr>\n"
else: else:
markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n" value = emphasize_header(value.strip('-').strip())
markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n"
else: else:
markdown_text += f"- {emoji} **{key}:**\n\n" markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n"
for item in value: else:
if isinstance(item, dict) and key.lower() == 'code feedback': if len(value.split()) > 1:
markdown_text += parse_code_suggestion(item, gfm_supported) markdown_text += f"{emoji} **{key_nice}:**\n\n {value}\n\n"
elif item: else:
markdown_text += f" - {item}\n" markdown_text += f"{emoji} **{key_nice}:** {value}\n\n"
if key.lower() == 'code feedback': if gfm_supported:
if gfm_supported: markdown_text += "</table>\n"
markdown_text += "</details>\n\n"
else: if 'code_feedback' in output_data:
markdown_text += "\n\n" if gfm_supported:
elif value != 'n/a': markdown_text += f"\n\n"
emoji = emojis.get(key, "") markdown_text += f"<details><summary> <strong>Code feedback:</strong></summary>\n\n"
markdown_text += f"- {emoji} **{key}:** {value}\n" markdown_text += "<hr>"
else:
markdown_text += f"\n\n** Code feedback:**\n\n"
for i, value in enumerate(output_data['code_feedback']):
if value is None or value == '' or value == {} or value == []:
continue
markdown_text += parse_code_suggestion(value, i, gfm_supported)+"\n\n"
if markdown_text.endswith('<hr>'):
markdown_text = markdown_text[:-4]
if gfm_supported:
markdown_text += f"</details>"
#print(markdown_text)
return markdown_text return markdown_text
def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str: def parse_code_suggestion(code_suggestion: dict, i: int = 0, gfm_supported: bool = True) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
Args: Args:
code_suggestions (dict): A dictionary containing data to be converted to markdown format. code_suggestion (dict): A dictionary containing data to be converted to markdown format.
Returns: Returns:
str: A string containing the markdown formatted text generated from the input dictionary. str: A string containing the markdown formatted text generated from the input dictionary.
""" """
markdown_text = "" markdown_text = ""
for sub_key, sub_value in code_suggestions.items(): if gfm_supported and 'relevant_line' in code_suggestion:
if isinstance(sub_value, dict): # "code example" markdown_text += '<table>'
markdown_text += f" - **{sub_key}:**\n" for sub_key, sub_value in code_suggestion.items():
for code_key, code_value in sub_value.items(): # 'before' and 'after' code try:
code_str = f"```\n{code_value}\n```" if sub_key.lower() == 'relevant_file':
code_str_indented = textwrap.indent(code_str, ' ') relevant_file = sub_value.strip('`').strip('"').strip("'")
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n" markdown_text += f"<tr><td>relevant file</td><td>{relevant_file}</td></tr>"
else: # continue
if "relevant file" in sub_key.lower(): elif sub_key.lower() == 'suggestion':
markdown_text += f"\n - **{sub_key}:** {sub_value} \n" markdown_text += (f"<tr><td>{sub_key} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>"
f"<td>\n\n<strong>\n\n{sub_value.strip()}\n\n</strong>\n</td></tr>")
elif sub_key.lower() == 'relevant_line':
markdown_text += f"<tr><td>relevant line</td>"
sub_value_list = sub_value.split('](')
relevant_line = sub_value_list[0].lstrip('`').lstrip('[')
if len(sub_value_list) > 1:
link = sub_value_list[1].rstrip(')').strip('`')
markdown_text += f"<td><a href='{link}'>{relevant_line}</a></td>"
else:
markdown_text += f"<td>{relevant_line}</td>"
markdown_text += "</tr>"
except Exception as e:
get_logger().exception(f"Failed to parse code suggestion: {e}")
pass
markdown_text += '</table>'
markdown_text += "<hr>"
else:
for sub_key, sub_value in code_suggestion.items():
if isinstance(sub_key, str):
sub_key = sub_key.rstrip()
if isinstance(sub_value,str):
sub_value = sub_value.rstrip()
if isinstance(sub_value, dict): # "code example"
markdown_text += f" - **{sub_key}:**\n"
for code_key, code_value in sub_value.items(): # 'before' and 'after' code
code_str = f"```\n{code_value}\n```"
code_str_indented = textwrap.indent(code_str, ' ')
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
else: else:
markdown_text += f" **{sub_key}:** {sub_value} \n" if "relevant_file" in sub_key.lower():
if not gfm_supported: markdown_text += f"\n - **{sub_key}:** {sub_value} \n"
if "relevant line" not in sub_key.lower(): # nicer presentation else:
# markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab markdown_text += f" **{sub_key}:** {sub_value} \n"
markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker 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
@ -288,19 +392,21 @@ def _fix_key_value(key: str, value: str):
return key, value return key, value
def load_yaml(response_text: str) -> dict: def load_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict:
response_text = response_text.removeprefix('```yaml').rstrip('`') response_text = response_text.removeprefix('```yaml').rstrip('`')
try: try:
data = yaml.safe_load(response_text) data = yaml.safe_load(response_text)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to parse AI prediction: {e}") get_logger().error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(response_text) data = try_fix_yaml(response_text, keys_fix_yaml=keys_fix_yaml)
return data return data
def try_fix_yaml(response_text: str) -> dict:
def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict:
response_text_lines = response_text.split('\n') response_text_lines = response_text.split('\n')
keys = ['relevant line:', 'suggestion content:', 'relevant file:'] keys = ['relevant line:', 'suggestion content:', 'relevant file:', 'existing code:', 'improved code:']
keys = keys + keys_fix_yaml
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...' # first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
response_text_lines_copy = response_text_lines.copy() response_text_lines_copy = response_text_lines.copy()
for i in range(0, len(response_text_lines_copy)): for i in range(0, len(response_text_lines_copy)):
@ -315,28 +421,40 @@ def try_fix_yaml(response_text: str) -> dict:
except: except:
get_logger().info(f"Failed to parse AI prediction after adding |-\n") get_logger().info(f"Failed to parse AI prediction after adding |-\n")
# second fallback - try to remove last lines # second fallback - try to extract only range from first ```yaml to ````
data = {} snippet_pattern = r'```(yaml)?[\s\S]*?```'
for i in range(1, len(response_text_lines)): snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
response_text_lines_tmp = '\n'.join(response_text_lines[:-i]) if snippet:
snippet_text = snippet.group()
try: try:
data = yaml.safe_load(response_text_lines_tmp,) data = yaml.safe_load(snippet_text.removeprefix('```yaml').rstrip('`'))
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines") get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet")
break return data
except: except:
pass pass
# thrid fallback - try to remove leading and trailing curly brackets # third fallback - try to remove leading and trailing curly brackets
response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}') response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}').rstrip(':\n')
try: try:
data = yaml.safe_load(response_text_copy,) data = yaml.safe_load(response_text_copy)
get_logger().info(f"Successfully parsed AI prediction after removing curly brackets") get_logger().info(f"Successfully parsed AI prediction after removing curly brackets")
return data return data
except: except:
pass pass
# fourth 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")
return data
except:
pass
def set_custom_labels(variables):
def set_custom_labels(variables, git_provider=None):
if not get_settings().config.enable_custom_labels: if not get_settings().config.enable_custom_labels:
return return
@ -348,15 +466,18 @@ def set_custom_labels(variables):
labels_list = f" - {labels_list}" if labels_list else "" labels_list = f" - {labels_list}" if labels_list else ""
variables["custom_labels"] = labels_list variables["custom_labels"] = labels_list
return return
#final_labels = ""
#for k, v in labels.items(): # Set custom labels
# 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):" variables["custom_labels_class"] = "class Label(str, Enum):"
counter = 0
labels_minimal_to_labels_dict = {}
for k, v in labels.items(): for k, v in labels.items():
description = v['description'].strip('\n').replace('\n', '\\n') description = "'" + v['description'].strip('\n').replace('\n', '\\n') + "'"
variables["custom_labels_class"] += f"\n {k.lower().replace(' ', '_')} = '{k}' # {description}" # variables["custom_labels_class"] += f"\n {k.lower().replace(' ', '_')} = '{k}' # {description}"
variables["custom_labels_class"] += f"\n {k.lower().replace(' ', '_')} = {description}"
labels_minimal_to_labels_dict[k.lower().replace(' ', '_')] = k
counter += 1
variables["labels_minimal_to_labels_dict"] = labels_minimal_to_labels_dict
def get_user_labels(current_labels: List[str] = None): def get_user_labels(current_labels: List[str] = None):
""" """
@ -374,7 +495,7 @@ def get_user_labels(current_labels: List[str] = None):
continue continue
user_labels.append(label) user_labels.append(label)
if user_labels: if user_labels:
get_logger().info(f"Keeping user labels: {user_labels}") get_logger().debug(f"Keeping user labels: {user_labels}")
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to get user labels: {e}") get_logger().exception(f"Failed to get user labels: {e}")
return current_labels return current_labels
@ -423,3 +544,84 @@ def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str:
except Exception as e: except Exception as e:
get_logger().warning(f"Failed to clip tokens: {e}") get_logger().warning(f"Failed to clip tokens: {e}")
return text return text
def replace_code_tags(text):
"""
Replace odd instances of ` with <code> and even instances of ` with </code>
"""
parts = text.split('`')
for i in range(1, len(parts), 2):
parts[i] = '<code>' + parts[i] + '</code>'
return ''.join(parts)
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
relevant_file: str,
relevant_line_in_file: str,
absolute_position: int = None) -> Tuple[int, int]:
position = -1
if absolute_position is None:
absolute_position = -1
re_hunk_header = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
for file in diff_files:
if file.filename and (file.filename.strip() == relevant_file):
patch = file.patch
patch_lines = patch.splitlines()
delta = 0
start1, size1, start2, size2 = 0, 0, 0, 0
if absolute_position != -1: # matching absolute to relative
for i, line in enumerate(patch_lines):
# new hunk
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
#
absolute_position_curr = start2 + delta - 1
if absolute_position_curr == absolute_position:
position = i
break
else:
# try to find the line in the patch using difflib, with some margin of error
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
patch_lines, n=3, cutoff=0.93)
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
relevant_line_in_file = matches_difflib[0]
for i, line in enumerate(patch_lines):
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
if relevant_line_in_file in line and line[0] != '-':
position = i
absolute_position = start2 + delta - 1
break
if position == -1 and relevant_line_in_file[0] == '+':
no_plus_line = relevant_line_in_file[1:].lstrip()
for i, line in enumerate(patch_lines):
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
if no_plus_line in line and line[0] != '-':
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
position = i
absolute_position = start2 + delta - 1
break
return position, absolute_position

View File

@ -6,7 +6,8 @@ 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 from pr_agent.log import setup_logger
setup_logger() log_level = os.environ.get("LOG_LEVEL", "INFO")
setup_logger(log_level)

View File

@ -18,6 +18,7 @@ global_settings = Dynaconf(
"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",
"settings/pr_line_questions_prompts.toml",
"settings/pr_description_prompts.toml", "settings/pr_description_prompts.toml",
"settings/pr_code_suggestions_prompts.toml", "settings/pr_code_suggestions_prompts.toml",
"settings/pr_sort_code_suggestions_prompts.toml", "settings/pr_sort_code_suggestions_prompts.toml",

View File

@ -1,32 +1,48 @@
import json import os
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
import os
from ..log import get_logger from ..log import get_logger
from ..algo.language_handler import is_valid_file
from ..algo.utils import clip_tokens, find_line_number_of_relevant_line_in_file, load_large_diff
from ..config_loader import get_settings
from .git_provider import GitProvider
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
AZURE_DEVOPS_AVAILABLE = True AZURE_DEVOPS_AVAILABLE = True
ADO_APP_CLIENT_DEFAULT_ID = "499b84ac-1321-427f-aa17-267ca6975798/.default"
MAX_PR_DESCRIPTION_AZURE_LENGTH = 4000-1
try: try:
# noinspection PyUnresolvedReferences
from msrest.authentication import BasicAuthentication from msrest.authentication import BasicAuthentication
# noinspection PyUnresolvedReferences
from azure.devops.connection import Connection from azure.devops.connection import Connection
from azure.devops.v7_1.git.models import Comment, CommentThread, GitVersionDescriptor, GitPullRequest # noinspection PyUnresolvedReferences
from azure.identity import DefaultAzureCredential
# noinspection PyUnresolvedReferences
from azure.devops.v7_1.git.models import (
Comment,
CommentThread,
GitVersionDescriptor,
GitPullRequest,
)
except ImportError: except ImportError:
AZURE_DEVOPS_AVAILABLE = False AZURE_DEVOPS_AVAILABLE = False
from ..config_loader import get_settings
from ..algo.utils import load_large_diff, clip_tokens
from ..algo.language_handler import is_valid_file
from .git_provider import EDIT_TYPE, FilePatchInfo
class AzureDevopsProvider(GitProvider):
class AzureDevopsProvider: def __init__(
def __init__(self, pr_url: Optional[str] = None, incremental: Optional[bool] = False): self, pr_url: Optional[str] = None, incremental: Optional[bool] = False
):
if not AZURE_DEVOPS_AVAILABLE: if not AZURE_DEVOPS_AVAILABLE:
raise ImportError("Azure DevOps provider is not available. Please install the required dependencies.") raise ImportError(
"Azure DevOps provider is not available. Please install the required dependencies."
)
self.azure_devops_client = self._get_azure_devops_client() self.azure_devops_client = self._get_azure_devops_client()
self.diff_files = None
self.workspace_slug = None self.workspace_slug = None
self.repo_slug = None self.repo_slug = None
self.repo = None self.repo = None
@ -37,9 +53,134 @@ class AzureDevopsProvider:
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
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:
for post_parameters in post_parameters_list:
comment = Comment(content=post_parameters["body"], comment_type=1)
thread = CommentThread(comments=[comment],
thread_context={
"filePath": post_parameters["path"],
"rightFileStart": {
"line": post_parameters["start_line"],
"offset": 1,
},
"rightFileEnd": {
"line": post_parameters["line"],
"offset": 1,
},
})
self.azure_devops_client.create_thread(
comment_thread=thread,
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num
)
if get_settings().config.verbosity_level >= 2:
get_logger().info(
f"Published code suggestion on {self.pr_num} at {post_parameters['path']}"
)
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 get_pr_description_full(self) -> str:
return self.pr.description
def edit_comment(self, comment, body: str):
try:
self.azure_devops_client.update_comment(
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
thread_id=comment["thread_id"],
comment_id=comment["comment_id"],
comment=Comment(content=body),
project=self.workspace_slug,
)
except Exception as e:
get_logger().exception(f"Failed to edit comment, error: {e}")
def remove_comment(self, comment):
try:
self.azure_devops_client.delete_comment(
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
thread_id=comment["thread_id"],
comment_id=comment["comment_id"],
project=self.workspace_slug,
)
except Exception as e:
get_logger().exception(f"Failed to remove comment, error: {e}")
def publish_labels(self, pr_types):
try:
for pr_type in pr_types:
self.azure_devops_client.create_pull_request_label(
label={"name": pr_type},
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}")
def get_pr_labels(self):
try:
labels = self.azure_devops_client.get_pull_request_labels(
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
return [label.name for label in labels]
except Exception as e:
get_logger().exception(f"Failed to get labels, error: {e}")
return []
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels', if capability in [
'remove_initial_comment', 'gfm_markdown']: "get_issue_comments",
]:
return False return False
return True return True
@ -49,49 +190,67 @@ class AzureDevopsProvider:
def get_repo_settings(self): def get_repo_settings(self):
try: try:
contents = self.azure_devops_client.get_item_content(repository_id=self.repo_slug, contents = self.azure_devops_client.get_item_content(
project=self.workspace_slug, download=False, repository_id=self.repo_slug,
include_content_metadata=False, include_content=True, project=self.workspace_slug,
path=".pr_agent.toml") download=False,
return contents include_content_metadata=False,
include_content=True,
path=".pr_agent.toml",
)
return list(contents)[0]
except Exception as e: except Exception as e:
get_logger().exception("get repo settings error") if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to get repo settings, error: {e}")
return "" return ""
def get_files(self): def get_files(self):
files = [] files = []
for i in self.azure_devops_client.get_pull_request_commits(project=self.workspace_slug, for i in self.azure_devops_client.get_pull_request_commits(
repository_id=self.repo_slug, project=self.workspace_slug,
pull_request_id=self.pr_num): repository_id=self.repo_slug,
pull_request_id=self.pr_num,
changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug, ):
repository_id=self.repo_slug, commit_id=i.commit_id) changes_obj = self.azure_devops_client.get_changes(
project=self.workspace_slug,
repository_id=self.repo_slug,
commit_id=i.commit_id,
)
for c in changes_obj.changes: for c in changes_obj.changes:
files.append(c['item']['path']) files.append(c["item"]["path"])
return list(set(files)) return list(set(files))
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
try: try:
if self.diff_files:
return self.diff_files
base_sha = self.pr.last_merge_target_commit base_sha = self.pr.last_merge_target_commit
head_sha = self.pr.last_merge_source_commit head_sha = self.pr.last_merge_source_commit
commits = self.azure_devops_client.get_pull_request_commits(project=self.workspace_slug, commits = self.azure_devops_client.get_pull_request_commits(
repository_id=self.repo_slug, project=self.workspace_slug,
pull_request_id=self.pr_num) repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
diff_files = [] diff_files = []
diffs = [] diffs = []
diff_types = {} diff_types = {}
for c in commits: for c in commits:
changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug, changes_obj = self.azure_devops_client.get_changes(
repository_id=self.repo_slug, commit_id=c.commit_id) project=self.workspace_slug,
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'): if i["item"]["gitObjectType"] == "tree":
continue 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"]
diffs = list(set(diffs)) diffs = list(set(diffs))
@ -99,118 +258,217 @@ class AzureDevopsProvider:
if not is_valid_file(file): if not is_valid_file(file):
continue continue
version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit') version = GitVersionDescriptor(
version=head_sha.commit_id, version_type="commit"
)
try: 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(
path=file, repository_id=self.repo_slug,
project=self.workspace_slug, path=file,
version_descriptor=version, project=self.workspace_slug,
download=False, version_descriptor=version,
include_content=True) download=False,
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: except Exception as error:
get_logger().error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(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 = "" new_file_content_str = ""
edit_type = EDIT_TYPE.MODIFIED edit_type = EDIT_TYPE.MODIFIED
if diff_types[file] == 'add': if diff_types[file] == "add":
edit_type = EDIT_TYPE.ADDED edit_type = EDIT_TYPE.ADDED
elif diff_types[file] == 'delete': elif diff_types[file] == "delete":
edit_type = EDIT_TYPE.DELETED edit_type = EDIT_TYPE.DELETED
elif diff_types[file] == 'rename': elif diff_types[file] == "rename":
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: 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(
path=file, repository_id=self.repo_slug,
project=self.workspace_slug, path=file,
version_descriptor=version, project=self.workspace_slug,
download=False, version_descriptor=version,
include_content=True) download=False,
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: except Exception as error:
get_logger().error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(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 = "" 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
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, )
patch=patch,
filename=file,
edit_type=edit_type))
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
patch=patch,
filename=file,
edit_type=edit_type,
)
)
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
except Exception as e: except Exception as e:
print(f"Error: {str(e)}") print(f"Error: {str(e)}")
return [] return []
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None):
comment = Comment(content=pr_comment) comment = Comment(content=pr_comment)
thread = CommentThread(comments=[comment]) thread = CommentThread(comments=[comment], thread_context=thread_context, status=1)
thread_response = self.azure_devops_client.create_thread(comment_thread=thread, project=self.workspace_slug, thread_response = self.azure_devops_client.create_thread(
repository_id=self.repo_slug, comment_thread=thread,
pull_request_id=self.pr_num) project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
response = {"thread_id": thread_response.id, "comment_id": thread_response.comments[0].id}
if is_temporary: if is_temporary:
self.temp_comments.append({'thread_id': thread_response.id, 'comment_id': comment.id}) self.temp_comments.append(response)
return response
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
usage_guide_text='<details> <summary><strong>✨ Describe tool usage guide:</strong></summary><hr>'
ind = pr_body.find(usage_guide_text)
if ind != -1:
pr_body = pr_body[:ind]
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
changes_walkthrough_text = '## **Changes walkthrough**'
ind = pr_body.find(changes_walkthrough_text)
if ind != -1:
pr_body = pr_body[:ind]
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
trunction_message = " ... (description truncated due to length limit)"
pr_body = pr_body[:MAX_PR_DESCRIPTION_AZURE_LENGTH - len(trunction_message)] + trunction_message
get_logger().warning("PR description was truncated due to length limit")
try: try:
updated_pr = GitPullRequest() updated_pr = GitPullRequest()
updated_pr.title = pr_title updated_pr.title = pr_title
updated_pr.description = pr_body updated_pr.description = pr_body
self.azure_devops_client.update_pull_request(project=self.workspace_slug, self.azure_devops_client.update_pull_request(
repository_id=self.repo_slug, project=self.workspace_slug,
pull_request_id=self.pr_num, repository_id=self.repo_slug,
git_pull_request_to_update=updated_pr) pull_request_id=self.pr_num,
git_pull_request_to_update=updated_pr,
)
except Exception as e: except Exception as e:
get_logger().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 try:
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 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):
raise NotImplementedError("Azure DevOps provider does not support publishing inline comment yet") self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)])
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError("Azure DevOps provider does not support creating inline comments yet")
def publish_inline_comments(self, comments: list[dict]): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str,
raise NotImplementedError("Azure DevOps provider does not support publishing inline comments yet") absolute_position: int = None):
position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(),
relevant_file.strip('`'),
relevant_line_in_file,
absolute_position)
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=position, absolute_position=absolute_position) if subject_type == "LINE" else {}
def publish_inline_comments(self, comments: list[dict], disable_fallback: bool = False):
overall_sucess = True
for comment in comments:
try:
self.publish_comment(comment["body"],
thread_context={
"filePath": comment["path"],
"rightFileStart": {
"line": comment["absolute_position"],
"offset": comment["position"],
},
"rightFileEnd": {
"line": comment["absolute_position"],
"offset": comment["position"],
},
})
if get_settings().config.verbosity_level >= 2:
get_logger().info(
f"Published code suggestion on {self.pr_num} at {comment['path']}"
)
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish code suggestion, error: {e}")
overall_sucess = False
return overall_sucess
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
def get_languages(self): def get_languages(self):
languages = [] languages = []
files = self.azure_devops_client.get_items(project=self.workspace_slug, repository_id=self.repo_slug, files = self.azure_devops_client.get_items(
recursion_level="Full", include_content_metadata=True, project=self.workspace_slug,
include_links=False, download=False) repository_id=self.repo_slug,
recursion_level="Full",
include_content_metadata=True,
include_links=False,
download=False,
)
for f in files: for f in files:
if f.git_object_type == 'blob': if f.git_object_type == "blob":
file_name, file_extension = os.path.splitext(f.path) file_name, file_extension = os.path.splitext(f.path)
languages.append(file_extension[1:]) languages.append(file_extension[1:])
extension_counts = {} extension_counts = {}
for ext in languages: for ext in languages:
if ext != '': if ext != "":
extension_counts[ext] = extension_counts.get(ext, 0) + 1 extension_counts[ext] = extension_counts.get(ext, 0) + 1
total_extensions = sum(extension_counts.values()) total_extensions = sum(extension_counts.values())
extension_percentages = {ext: (count / total_extensions) * 100 for ext, count in extension_counts.items()} extension_percentages = {
ext: (count / total_extensions) * 100
for ext, count in extension_counts.items()
}
return extension_percentages return extension_percentages
def get_pr_branch(self): def get_pr_branch(self):
pr_info = self.azure_devops_client.get_pull_request_by_id(project=self.workspace_slug, pr_info = self.azure_devops_client.get_pull_request_by_id(
pull_request_id=self.pr_num) project=self.workspace_slug, pull_request_id=self.pr_num
source_branch = pr_info.source_ref_name.split('/')[-1] )
source_branch = pr_info.source_ref_name.split("/")[-1]
return source_branch return source_branch
def get_pr_description(self): def get_pr_description(self, *, full: bool = True) -> str:
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None) max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
if max_tokens: if max_tokens:
return clip_tokens(self.pr.description, max_tokens) return clip_tokens(self.pr.description, max_tokens)
@ -220,25 +478,26 @@ class AzureDevopsProvider:
return 0 return 0
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError("Azure DevOps provider does not support issue comments yet") raise NotImplementedError(
"Azure DevOps provider does not support issue comments yet"
)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
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:
return True return True
def get_issue_comments(self):
raise NotImplementedError("Azure DevOps provider does not support issue comments yet")
@staticmethod @staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]: def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]:
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_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":
raise ValueError("The provided URL does not appear to be a Azure DevOps PR URL") raise ValueError(
"The provided URL does not appear to be a Azure DevOps PR URL"
)
workspace_slug = path_parts[1] workspace_slug = path_parts[1]
repo_slug = path_parts[3] repo_slug = path_parts[3]
@ -249,15 +508,32 @@ class AzureDevopsProvider:
return workspace_slug, repo_slug, pr_number return workspace_slug, repo_slug, pr_number
def _get_azure_devops_client(self): @staticmethod
try: def _get_azure_devops_client():
pat = get_settings().azure_devops.pat org = get_settings().azure_devops.get("org", None)
org = get_settings().azure_devops.org pat = get_settings().azure_devops.get("pat", None)
except AttributeError as e:
raise ValueError(
"Azure DevOps PAT token is required ") from e
credentials = BasicAuthentication('', pat) if not org:
raise ValueError("Azure DevOps organization is required")
if pat:
auth_token = pat
else:
try:
# try to use azure default credentials
# see https://learn.microsoft.com/en-us/python/api/overview/azure/identity-readme?view=azure-python
# for usage and env var configuration of user-assigned managed identity, local machine auth etc.
get_logger().info("No PAT found in settings, trying to use Azure Default Credentials.")
credentials = DefaultAzureCredential()
accessToken = credentials.get_token(ADO_APP_CLIENT_DEFAULT_ID)
auth_token = accessToken.token
except Exception as e:
get_logger().error(f"No PAT found in settings, and Azure Default Authentication failed, error: {e}")
raise
credentials = BasicAuthentication("", auth_token)
credentials = BasicAuthentication("", auth_token)
azure_devops_connection = Connection(base_url=org, creds=credentials) azure_devops_connection = Connection(base_url=org, creds=credentials)
azure_devops_client = azure_devops_connection.clients.get_git_client() azure_devops_client = azure_devops_connection.clients.get_git_client()
@ -265,13 +541,26 @@ class AzureDevopsProvider:
def _get_repo(self): def _get_repo(self):
if self.repo is None: if self.repo is None:
self.repo = self.azure_devops_client.get_repository(project=self.workspace_slug, self.repo = self.azure_devops_client.get_repository(
repository_id=self.repo_slug) project=self.workspace_slug, repository_id=self.repo_slug
)
return self.repo return self.repo
def _get_pr(self): def _get_pr(self):
self.pr = self.azure_devops_client.get_pull_request_by_id(pull_request_id=self.pr_num, project=self.workspace_slug) self.pr = self.azure_devops_client.get_pull_request_by_id(
pull_request_id=self.pr_num, project=self.workspace_slug
)
return self.pr return self.pr
def get_commit_messages(self): def get_commit_messages(self):
return "" # not implemented yet return "" # not implemented yet
def get_pr_id(self):
try:
pr_id = f"{self.workspace_slug}/{self.repo_slug}/{self.pr_num}"
return pr_id
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to get pr id, error: {e}")
return ""

View File

@ -6,10 +6,11 @@ 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 pr_agent.algo.types import FilePatchInfo, EDIT_TYPE
from ..algo.utils import find_line_number_of_relevant_line_in_file
from ..config_loader import get_settings from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from .git_provider import GitProvider
class BitbucketProvider(GitProvider): class BitbucketProvider(GitProvider):
@ -159,7 +160,11 @@ class BitbucketProvider(GitProvider):
def get_comment_url(self, comment): def get_comment_url(self, comment):
return comment.data['links']['html']['href'] return comment.data['links']['html']['href']
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True): def publish_persistent_comment(self, pr_comment: str,
initial_header: str,
update_header: bool = True,
name='review',
final_update_message=True):
try: try:
for comment in self.pr.comments(): for comment in self.pr.comments():
body = comment.raw body = comment.raw
@ -167,15 +172,16 @@ class BitbucketProvider(GitProvider):
latest_commit_url = self.get_latest_commit_url() latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment) comment_url = self.get_comment_url(comment)
if update_header: if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n" updated_header = f"{initial_header}\n\n### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header) pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else: else:
pr_comment_updated = pr_comment pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message") get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
d = {"content": {"raw": pr_comment_updated}} d = {"content": {"raw": pr_comment_updated}}
response = comment._update_data(comment.put(None, data=d)) response = comment._update_data(comment.put(None, data=d))
self.publish_comment( if final_update_message:
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}") self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return return
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}") get_logger().exception(f"Failed to update persistent review, error: {e}")
@ -186,6 +192,13 @@ class BitbucketProvider(GitProvider):
comment = self.pr.comment(pr_comment) comment = self.pr.comment(pr_comment)
if is_temporary: if is_temporary:
self.temp_comments.append(comment["id"]) self.temp_comments.append(comment["id"])
return comment
def edit_comment(self, comment, body: str):
try:
comment.update(body)
except Exception as e:
get_logger().exception(f"Failed to update comment, error: {e}")
def remove_initial_comment(self): def remove_initial_comment(self):
try: try:
@ -201,8 +214,10 @@ class BitbucketProvider(GitProvider):
get_logger().exception(f"Failed to remove comment, error: {e}") get_logger().exception(f"Failed to remove comment, error: {e}")
# funtion to create_inline_comment # funtion to create_inline_comment
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, absolute_position: int = None):
position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), relevant_file.strip('`'), relevant_line_in_file) position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(),
relevant_file.strip('`'),
relevant_line_in_file, absolute_position)
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().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}")
@ -237,8 +252,8 @@ class BitbucketProvider(GitProvider):
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant_file'].strip('`').strip("'").rstrip()
relevant_line_str = suggestion['relevant line'] relevant_line_str = suggestion['relevant_line'].rstrip()
if not relevant_line_str: if not relevant_line_str:
return "" return ""
@ -288,7 +303,7 @@ class BitbucketProvider(GitProvider):
"Bitbucket provider does not support issue comments yet" "Bitbucket provider does not support issue comments yet"
) )
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
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:
@ -327,6 +342,41 @@ class BitbucketProvider(GitProvider):
def _get_pr(self): def _get_pr(self):
return self._get_repo().pullrequests.get(self.pr_num) return self._get_repo().pullrequests.get(self.pr_num)
def get_pr_file_content(self, file_path: str, branch: str) -> str:
try:
if branch == self.pr.source_branch:
branch = self.pr.data["source"]["commit"]["hash"]
elif branch == self.pr.destination_branch:
branch = self.pr.data["destination"]["commit"]["hash"]
url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/"
f"{branch}/{file_path}")
response = requests.request("GET", url, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text
return contents
except Exception:
return ""
def create_or_update_pr_file(self, file_path: str, branch: str, contents="", message="") -> None:
url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/")
if not message:
if contents:
message = f"Update {file_path}"
else:
message = f"Create {file_path}"
files={file_path: contents}
data={
"message": message,
"branch": branch
}
headers = {'Authorization':self.headers['Authorization']} if 'Authorization' in self.headers else {}
try:
requests.request("POST", url, headers=headers, data=data, files=files)
except Exception:
get_logger().exception(f"Failed to create empty file {file_path} in branch {branch}")
def _get_pr_file_content(self, remote_link: str): def _get_pr_file_content(self, remote_link: str):
return "" return ""
@ -354,5 +404,5 @@ class BitbucketProvider(GitProvider):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_labels(self): def get_pr_labels(self):
pass pass

View File

@ -6,9 +6,9 @@ import requests
from atlassian.bitbucket import Bitbucket from atlassian.bitbucket import Bitbucket
from starlette_context import context from starlette_context import context
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from .git_provider import GitProvider
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file from pr_agent.algo.types import FilePatchInfo
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff, find_line_number_of_relevant_line_in_file
from ..config_loader import get_settings from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
@ -210,11 +210,14 @@ class BitbucketServerProvider(GitProvider):
pass pass
# funtion to create_inline_comment # funtion to create_inline_comment
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,
absolute_position: int = None):
position, absolute_position = find_line_number_of_relevant_line_in_file( position, absolute_position = find_line_number_of_relevant_line_in_file(
self.get_diff_files(), self.get_diff_files(),
relevant_file.strip('`'), relevant_file.strip('`'),
relevant_line_in_file relevant_line_in_file,
absolute_position
) )
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
@ -243,8 +246,8 @@ class BitbucketServerProvider(GitProvider):
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant_file'].strip('`').strip("'").rstrip()
relevant_line_str = suggestion['relevant line'] relevant_line_str = suggestion['relevant_line'].rstrip()
if not relevant_line_str: if not relevant_line_str:
return "" return ""
@ -285,7 +288,7 @@ class BitbucketServerProvider(GitProvider):
"Bitbucket provider does not support issue comments yet" "Bitbucket provider does not support issue comments yet"
) )
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
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:
@ -344,7 +347,7 @@ class BitbucketServerProvider(GitProvider):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_labels(self): def get_pr_labels(self):
pass pass
def _get_pr_comments_url(self): def _get_pr_comments_url(self):

View File

@ -5,9 +5,9 @@ from typing import List, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from pr_agent.git_providers.codecommit_client import CodeCommitClient from pr_agent.git_providers.codecommit_client import CodeCommitClient
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from .git_provider import GitProvider
from ..config_loader import get_settings from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
@ -216,7 +216,7 @@ class CodeCommitProvider(GitProvider):
def publish_labels(self, labels): def publish_labels(self, labels):
return [""] # not implemented yet return [""] # not implemented yet
def get_labels(self): def get_pr_labels(self):
return [""] # not implemented yet return [""] # not implemented yet
def remove_initial_comment(self): def remove_initial_comment(self):
@ -229,9 +229,6 @@ class CodeCommitProvider(GitProvider):
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html # 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):
raise NotImplementedError("CodeCommit provider does not support creating inline comments yet")
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
@ -300,7 +297,7 @@ class CodeCommitProvider(GitProvider):
settings_filename = ".pr_agent.toml" settings_filename = ".pr_agent.toml"
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, disable_eyes: bool = False) -> Optional[int]:
get_logger().info("CodeCommit provider does not support eyes reaction yet") get_logger().info("CodeCommit provider does not support eyes reaction yet")
return True return True

View File

@ -13,7 +13,8 @@ import urllib3.util
from git import Repo from git import Repo
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, FilePatchInfo, GitProvider from pr_agent.git_providers.git_provider import GitProvider
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from pr_agent.git_providers.local_git_provider import PullRequestMimic from pr_agent.git_providers.local_git_provider import PullRequestMimic
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -207,11 +208,11 @@ class GerritProvider(GitProvider):
Comment = namedtuple('Comment', ['body']) Comment = namedtuple('Comment', ['body'])
return Comments([Comment(c['message']) for c in reversed(comments)]) return Comments([Comment(c['message']) for c in reversed(comments)])
def get_labels(self): def get_pr_labels(self):
raise NotImplementedError( raise NotImplementedError(
'Getting labels is not implemented for the gerrit provider') 'Getting labels is not implemented for the gerrit provider')
def add_eyes_reaction(self, issue_comment_id: int): def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False):
raise NotImplementedError( raise NotImplementedError(
'Adding reactions is not implemented for the gerrit provider') 'Adding reactions is not implemented for the gerrit provider')
@ -380,11 +381,6 @@ class GerritProvider(GitProvider):
'Publishing inline comments is not implemented for the gerrit ' 'Publishing inline comments is not implemented for the gerrit '
'provider') '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): def publish_labels(self, labels):
# Not applicable to the local git provider, # Not applicable to the local git provider,

View File

@ -1,35 +1,13 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
from enum import Enum
from typing import Optional from typing import Optional
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.algo.types import FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
class EDIT_TYPE(Enum):
ADDED = 1
DELETED = 2
MODIFIED = 3
RENAMED = 4
UNKNOWN = 5
@dataclass
class FilePatchInfo:
base_file: str
head_file: str
patch: str
filename: str
tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
old_filename: str = None
num_plus_lines: int = -1
num_minus_lines: int = -1
class GitProvider(ABC): class GitProvider(ABC):
@abstractmethod @abstractmethod
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
@ -63,6 +41,12 @@ class GitProvider(ABC):
def get_pr_description_full(self) -> str: def get_pr_description_full(self) -> str:
pass pass
def edit_comment(self, comment, body: str):
pass
def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
pass
def get_pr_description(self, *, full: bool = True) -> str: def get_pr_description(self, *, full: bool = True) -> str:
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.algo.utils import clip_tokens from pr_agent.algo.utils import clip_tokens
@ -73,16 +57,57 @@ class GitProvider(ABC):
return description return description
def get_user_description(self) -> str: def get_user_description(self) -> str:
if hasattr(self, 'user_description') and not (self.user_description is None):
return self.user_description
description = (self.get_pr_description_full() or "").strip() description = (self.get_pr_description_full() or "").strip()
description_lowercase = description.lower()
get_logger().debug(f"Existing description", description=description_lowercase)
# 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 any(description.startswith(header) for header in ("## PR Type", "## PR Description")): if not self._is_generated_by_pr_agent(description_lowercase):
get_logger().info(f"Existing description was not generated by the pr-agent")
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 a 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
if "## User Description:" not in description: user_description_header = "## **user description**"
if user_description_header not in description_lowercase:
get_logger().info(f"Existing description was generated by the pr-agent, but it doesn't contain a user description")
return "" return ""
# otherwise, extract the original user description from the existing pr-agent description and return it # otherwise, extract the original user description from the existing pr-agent description and return it
return description.split("## User Description:", 1)[1].strip() # user_description_start_position = description_lowercase.find(user_description_header) + len(user_description_header)
# return description[user_description_start_position:].split("\n", 1)[-1].strip()
# the 'user description' is in the beginning. extract and return it
possible_headers = self._possible_headers()
start_position = description_lowercase.find(user_description_header) + len(user_description_header)
end_position = len(description)
for header in possible_headers: # try to clip at the next header
if header != user_description_header and header in description_lowercase:
end_position = min(end_position, description_lowercase.find(header))
if end_position != len(description) and end_position > start_position:
original_user_description = description[start_position:end_position].strip()
if original_user_description.endswith("___"):
original_user_description = original_user_description[:-3].strip()
else:
original_user_description = description.split("___")[0].strip()
if original_user_description.lower().startswith(user_description_header):
original_user_description = original_user_description[len(user_description_header):].strip()
get_logger().info(f"Extracted user description from existing description",
description=original_user_description)
self.user_description = original_user_description
return original_user_description
def _possible_headers(self):
return ("## **user description**", "## **pr type**", "## **pr description**", "## **pr labels**", "## **type**", "## **description**",
"## **labels**", "### 🤖 generated by pr agent")
def _is_generated_by_pr_agent(self, description_lowercase: str) -> bool:
possible_headers = self._possible_headers()
return any(description_lowercase.startswith(header) for header in possible_headers)
@abstractmethod @abstractmethod
def get_repo_settings(self): def get_repo_settings(self):
@ -99,16 +124,20 @@ class GitProvider(ABC):
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
pass pass
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool): def publish_persistent_comment(self, pr_comment: str,
initial_header: str,
update_header: bool = True,
name='review',
final_update_message=True):
self.publish_comment(pr_comment) self.publish_comment(pr_comment)
@abstractmethod @abstractmethod
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):
pass pass
@abstractmethod 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): absolute_position: int = None):
pass raise NotImplementedError("This git provider does not support creating inline comments yet")
@abstractmethod @abstractmethod
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
@ -135,11 +164,14 @@ class GitProvider(ABC):
pass pass
@abstractmethod @abstractmethod
def get_labels(self): def get_pr_labels(self):
pass
def get_repo_labels(self):
pass pass
@abstractmethod @abstractmethod
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
pass pass
@abstractmethod @abstractmethod
@ -151,9 +183,21 @@ class GitProvider(ABC):
def get_commit_messages(self): def get_commit_messages(self):
pass pass
def get_pr_url(self) -> str:
if hasattr(self, 'pr_url'):
return self.pr_url
return ""
def get_latest_commit_url(self) -> str: def get_latest_commit_url(self) -> str:
return "" return ""
def auto_approve(self) -> bool:
return False
def calc_pr_statistics(self, pull_request_data: dict):
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.
@ -222,7 +266,6 @@ def get_main_pr_language(languages, files) -> str:
return main_language_str return main_language_str
class IncrementalPR: 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

View File

@ -1,3 +1,4 @@
import time
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from typing import Optional, Tuple from typing import Optional, Tuple
@ -8,12 +9,12 @@ from retry import retry
from starlette_context import context from starlette_context import context
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file from ..algo.utils import load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff, clip_tokens
from ..config_loader import get_settings from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
from ..servers.utils import RateLimitExceeded from ..servers.utils import RateLimitExceeded
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR, EDIT_TYPE from .git_provider import GitProvider, IncrementalPR
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
class GithubProvider(GitProvider): class GithubProvider(GitProvider):
@ -23,6 +24,8 @@ class GithubProvider(GitProvider):
self.installation_id = context.get("installation_id", None) self.installation_id = context.get("installation_id", None)
except Exception: except Exception:
self.installation_id = None self.installation_id = None
self.base_url = get_settings().get("GITHUB.BASE_URL", "https://api.github.com").rstrip("/")
self.base_url_html = self.base_url.split("api/")[0].rstrip("/") if "api/" in self.base_url else "https://github.com"
self.github_client = self._get_github_client() self.github_client = self._get_github_client()
self.repo = None self.repo = None
self.pr_num = None self.pr_num = None
@ -33,48 +36,54 @@ class GithubProvider(GitProvider):
self.incremental = incremental self.incremental = incremental
if pr_url and 'pull' in pr_url: if pr_url and 'pull' in pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
self.last_commit_id = list(self.pr.get_commits())[-1] self.pr_commits = list(self.pr.get_commits())
if self.incremental.is_incremental:
self.unreviewed_files_set = dict()
self.get_incremental_commits()
self.last_commit_id = self.pr_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 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
else:
self.pr_commits = None
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
return True return True
def get_pr_url(self) -> str: def get_pr_url(self) -> str:
return f"https://github.com/{self.repo}/pull/{self.pr_num}" return self.pr.html_url
def set_pr(self, pr_url: str): def set_pr(self, pr_url: str):
self.repo, self.pr_num = self._parse_pr_url(pr_url) self.repo, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr() self.pr = self._get_pr()
if self.incremental.is_incremental:
self.get_incremental_commits()
def get_incremental_commits(self): def get_incremental_commits(self):
self.commits = list(self.pr.get_commits()) if not self.pr_commits:
self.pr_commits = list(self.pr.get_commits())
self.previous_review = self.get_previous_review(full=True, incremental=True) 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()
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}'"):
get_logger().info(f"Skipping merge commit {commit.commit.message}") get_logger().info(f"Skipping merge commit {commit.commit.message}")
continue continue
self.file_set.update({file.filename: file for file in commit.files}) self.unreviewed_files_set.update({file.filename: file for file in commit.files})
else: else:
raise ValueError("No previous review found") get_logger().info("No previous review found, will review the entire PR")
self.incremental.is_incremental = False
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 = None first_new_commit_index = None
for index in range(len(self.commits) - 1, -1, -1): for index in range(len(self.pr_commits) - 1, -1, -1):
if self.commits[index].commit.author.date > last_review_time: if self.pr_commits[index].commit.author.date > last_review_time:
self.incremental.first_new_commit = self.commits[index] self.incremental.first_new_commit = self.pr_commits[index]
first_new_commit_index = index first_new_commit_index = index
else: else:
self.incremental.last_seen_commit = self.commits[index] self.incremental.last_seen_commit = self.pr_commits[index]
break break
return self.commits[first_new_commit_index:] if first_new_commit_index is not None else [] return self.pr_commits[first_new_commit_index:] if first_new_commit_index is not None else []
def get_previous_review(self, *, full: bool, incremental: bool): def get_previous_review(self, *, full: bool, incremental: bool):
if not (full or incremental): if not (full or incremental):
@ -83,7 +92,7 @@ class GithubProvider(GitProvider):
self.comments = list(self.pr.get_issue_comments()) self.comments = list(self.pr.get_issue_comments())
prefixes = [] prefixes = []
if full: if full:
prefixes.append("## PR Analysis") prefixes.append("## PR Review")
if incremental: if incremental:
prefixes.append("## Incremental PR Review") 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):
@ -91,12 +100,20 @@ class GithubProvider(GitProvider):
return self.comments[index] return self.comments[index]
def get_files(self): def get_files(self):
if self.incremental.is_incremental and self.file_set: if self.incremental.is_incremental and self.unreviewed_files_set:
return self.file_set.values() return self.unreviewed_files_set.values()
if not self.git_files: try:
# bring files from GitHub only once git_files = context.get("git_files", None)
if git_files:
return git_files
self.git_files = self.pr.get_files() self.git_files = self.pr.get_files()
return self.git_files context["git_files"] = self.git_files
return self.git_files
except Exception:
if not self.git_files:
self.git_files = self.pr.get_files()
return self.git_files
@retry(exceptions=RateLimitExceeded, @retry(exceptions=RateLimitExceeded,
tries=get_settings().github.ratelimit_retries, delay=2, backoff=2, jitter=(1, 3)) tries=get_settings().github.ratelimit_retries, delay=2, backoff=2, jitter=(1, 3))
@ -110,6 +127,13 @@ class GithubProvider(GitProvider):
or renamed files in the merge request. or renamed files in the merge request.
""" """
try: try:
try:
diff_files = context.get("diff_files", None)
if diff_files:
return diff_files
except Exception:
pass
if self.diff_files: if self.diff_files:
return self.diff_files return self.diff_files
@ -123,10 +147,10 @@ class GithubProvider(GitProvider):
new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha) # communication with GitHub new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha) # communication with GitHub
patch = file.patch patch = file.patch
if self.incremental.is_incremental and self.file_set: if self.incremental.is_incremental and self.unreviewed_files_set:
original_file_content_str = self._get_pr_file_content(file, self.incremental.last_seen_commit_sha) original_file_content_str = self._get_pr_file_content(file, self.incremental.last_seen_commit_sha)
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)
self.file_set[file.filename] = patch self.unreviewed_files_set[file.filename] = patch
else: else:
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha) original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
if not patch: if not patch:
@ -155,6 +179,11 @@ class GithubProvider(GitProvider):
diff_files.append(file_patch_canonical_structure) diff_files.append(file_patch_canonical_structure)
self.diff_files = diff_files self.diff_files = diff_files
try:
context["diff_files"] = diff_files
except Exception:
pass
return diff_files return diff_files
except GithubException.RateLimitExceededException as e: except GithubException.RateLimitExceededException as e:
@ -170,7 +199,11 @@ class GithubProvider(GitProvider):
def get_comment_url(self, comment) -> str: def get_comment_url(self, comment) -> str:
return comment.html_url return comment.html_url
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True): def publish_persistent_comment(self, pr_comment: str,
initial_header: str,
update_header: bool = True,
name='review',
final_update_message=True):
prev_comments = list(self.pr.get_issue_comments()) prev_comments = list(self.pr.get_issue_comments())
for comment in prev_comments: for comment in prev_comments:
body = comment.body body = comment.body
@ -178,14 +211,15 @@ class GithubProvider(GitProvider):
latest_commit_url = self.get_latest_commit_url() latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment) comment_url = self.get_comment_url(comment)
if update_header: if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n" updated_header = f"{initial_header}\n\n### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header) pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else: else:
pr_comment_updated = pr_comment pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message") get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
response = comment.edit(pr_comment_updated) response = comment.edit(pr_comment_updated)
self.publish_comment( if final_update_message:
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}") self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return return
self.publish_comment(pr_comment) self.publish_comment(pr_comment)
@ -201,13 +235,18 @@ class GithubProvider(GitProvider):
if not hasattr(self.pr, 'comments_list'): if not hasattr(self.pr, 'comments_list'):
self.pr.comments_list = [] self.pr.comments_list = []
self.pr.comments_list.append(response) self.pr.comments_list.append(response)
return response
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):
self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)]) self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)])
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): 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.diff_files, relevant_file.strip('`'), relevant_line_in_file) absolute_position: int = None):
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files,
relevant_file.strip('`'),
relevant_line_in_file,
absolute_position)
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().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}")
@ -217,8 +256,114 @@ class GithubProvider(GitProvider):
path = relevant_file.strip() path = relevant_file.strip()
return dict(body=body, path=path, position=position) if subject_type == "LINE" else {} return dict(body=body, path=path, position=position) if subject_type == "LINE" else {}
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict], disable_fallback: bool = False):
self.pr.create_review(commit=self.last_commit_id, comments=comments) try:
# publish all comments in a single message
self.pr.create_review(commit=self.last_commit_id, comments=comments)
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish inline comments")
if (getattr(e, "status", None) == 422
and get_settings().github.publish_inline_comments_fallback_with_verification and not disable_fallback):
pass # continue to try _publish_inline_comments_fallback_with_verification
else:
raise e # will end up with publishing the comments one by one
try:
self._publish_inline_comments_fallback_with_verification(comments)
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish inline code comments fallback, error: {e}")
raise e
def _publish_inline_comments_fallback_with_verification(self, comments: list[dict]):
"""
Check each inline comment separately against the GitHub API and discard of invalid comments,
then publish all the remaining valid comments in a single review.
For invalid comments, also try removing the suggestion part and posting the comment just on the first line.
"""
verified_comments, invalid_comments = self._verify_code_comments(comments)
# publish as a group the verified comments
if verified_comments:
try:
self.pr.create_review(commit=self.last_commit_id, comments=verified_comments)
except:
pass
# try to publish one by one the invalid comments as a one-line code comment
if invalid_comments and get_settings().github.try_fix_invalid_inline_comments:
fixed_comments_as_one_liner = self._try_fix_invalid_inline_comments(
[comment for comment, _ in invalid_comments])
for comment in fixed_comments_as_one_liner:
try:
self.publish_inline_comments([comment], disable_fallback=True)
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Published invalid comment as a single line comment: {comment}")
except:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish invalid comment as a single line comment: {comment}")
def _verify_code_comment(self, comment: dict):
is_verified = False
e = None
try:
# event ="" # By leaving this blank, you set the review action state to PENDING
input = dict(commit_id=self.last_commit_id.sha, comments=[comment])
headers, data = self.pr._requester.requestJsonAndCheck(
"POST", f"{self.pr.url}/reviews", input=input)
pending_review_id = data["id"]
is_verified = True
except Exception as err:
is_verified = False
pending_review_id = None
e = err
if pending_review_id is not None:
try:
self.pr._requester.requestJsonAndCheck("DELETE", f"{self.pr.url}/reviews/{pending_review_id}")
except Exception:
pass
return is_verified, e
def _verify_code_comments(self, comments: list[dict]) -> tuple[list[dict], list[tuple[dict, Exception]]]:
"""Very each comment against the GitHub API and return 2 lists: 1 of verified and 1 of invalid comments"""
verified_comments = []
invalid_comments = []
for comment in comments:
time.sleep(1) # for avoiding secondary rate limit
is_verified, e = self._verify_code_comment(comment)
if is_verified:
verified_comments.append(comment)
else:
invalid_comments.append((comment, e))
return verified_comments, invalid_comments
def _try_fix_invalid_inline_comments(self, invalid_comments: list[dict]) -> list[dict]:
"""
Try fixing invalid comments by removing the suggestion part and setting the comment just on the first line.
Return only comments that have been modified in some way.
This is a best-effort attempt to fix invalid comments, and should be verified accordingly.
"""
import copy
fixed_comments = []
for comment in invalid_comments:
try:
fixed_comment = copy.deepcopy(comment) # avoid modifying the original comment dict for later logging
if "```suggestion" in comment["body"]:
fixed_comment["body"] = comment["body"].split("```suggestion")[0]
if "start_line" in comment:
fixed_comment["line"] = comment["start_line"]
del fixed_comment["start_line"]
if "start_side" in comment:
fixed_comment["side"] = comment["start_side"]
del fixed_comment["start_side"]
if fixed_comment != comment:
fixed_comments.append(fixed_comment)
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to fix inline comment, error: {e}")
return fixed_comments
def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_code_suggestions(self, code_suggestions: list) -> bool:
""" """
@ -262,13 +407,26 @@ class GithubProvider(GitProvider):
post_parameters_list.append(post_parameters) post_parameters_list.append(post_parameters)
try: try:
self.pr.create_review(commit=self.last_commit_id, comments=post_parameters_list) self.publish_inline_comments(post_parameters_list)
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:
get_logger().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 edit_comment(self, comment, body: str):
comment.edit(body=body)
def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
try:
# self.pr.get_issue_comment(comment_id).edit(body)
headers, data_patch = self.pr._requester.requestJsonAndCheck(
"POST", f"{self.base_url}/repos/{self.repo}/pulls/{self.pr_num}/comments/{comment_id}/replies",
input={"body": body}
)
except Exception as e:
get_logger().exception(f"Failed to reply comment, error: {e}")
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', []):
@ -327,23 +485,31 @@ class GithubProvider(GitProvider):
except Exception: except Exception:
return "" return ""
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
if disable_eyes:
return None
try: try:
reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes") headers, data_patch = self.pr._requester.requestJsonAndCheck(
return reaction.id "POST", f"{self.base_url}/repos/{self.repo}/issues/comments/{issue_comment_id}/reactions",
input={"content": "eyes"}
)
return data_patch.get("id", None)
except Exception as e: except Exception as e:
get_logger().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: str) -> bool:
try: try:
self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id) # self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id)
headers, data_patch = self.pr._requester.requestJsonAndCheck(
"DELETE",
f"{self.base_url}/repos/{self.repo}/issues/comments/{issue_comment_id}/reactions/{reaction_id}"
)
return True return True
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to remove eyes reaction, error: {e}") get_logger().exception(f"Failed to remove eyes reaction, error: {e}")
return False return False
@staticmethod @staticmethod
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)
@ -415,7 +581,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, base_url=get_settings().github.base_url) return Github(app_auth=auth, base_url=self.base_url)
if deployment_type == 'user': if deployment_type == 'user':
try: try:
@ -424,7 +590,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), base_url=get_settings().github.base_url) return Github(auth=Auth.Token(token), base_url=self.base_url)
def _get_repo(self): def _get_repo(self):
if hasattr(self, 'repo_obj') and \ if hasattr(self, 'repo_obj') and \
@ -439,13 +605,36 @@ class GithubProvider(GitProvider):
def _get_pr(self): def _get_pr(self):
return self._get_repo().get_pull(self.pr_num) return self._get_repo().get_pull(self.pr_num)
def _get_pr_file_content(self, file: FilePatchInfo, sha: str) -> str: def get_pr_file_content(self, file_path: str, branch: str) -> str:
try: try:
file_content_str = str(self._get_repo().get_contents(file.filename, ref=sha).decoded_content.decode()) file_content_str = str(
self._get_repo()
.get_contents(file_path, ref=branch)
.decoded_content.decode()
)
except Exception: except Exception:
file_content_str = "" file_content_str = ""
return file_content_str return file_content_str
def create_or_update_pr_file(
self, file_path: str, branch: str, contents="", message=""
) -> None:
try:
file_obj = self._get_repo().get_contents(file_path, ref=branch)
sha1=file_obj.sha
except Exception:
sha1=""
self.repo_obj.update_file(
path=file_path,
message=message,
content=contents,
sha=sha1,
branch=branch,
)
def _get_pr_file_content(self, file: FilePatchInfo, sha: str) -> str:
return self.get_pr_file_content(file.filename, sha)
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",
@ -461,13 +650,17 @@ class GithubProvider(GitProvider):
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().exception(f"Failed to publish labels, error: {e}")
def get_labels(self): def get_pr_labels(self):
try: try:
return [label.name for label in self.pr.labels] return [label.name for label in self.pr.labels]
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to get labels, error: {e}") get_logger().exception(f"Failed to get labels, error: {e}")
return [] return []
def get_repo_labels(self):
labels = self.repo_obj.get_labels()
return [label for label in labels]
def get_commit_messages(self): def get_commit_messages(self):
""" """
Retrieves the commit messages of a pull request. Retrieves the commit messages of a pull request.
@ -488,8 +681,8 @@ class GithubProvider(GitProvider):
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant_file'].strip('`').strip("'").strip('\n')
relevant_line_str = suggestion['relevant line'] relevant_line_str = suggestion['relevant_line'].strip('\n')
if not relevant_line_str: if not relevant_line_str:
return "" return ""
@ -503,7 +696,7 @@ class GithubProvider(GitProvider):
# link to diff # link to diff
sha_file = hashlib.sha256(relevant_file.encode('utf-8')).hexdigest() sha_file = hashlib.sha256(relevant_file.encode('utf-8')).hexdigest()
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{absolute_position}" link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{absolute_position}"
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:
@ -514,11 +707,11 @@ class GithubProvider(GitProvider):
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str: 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() sha_file = hashlib.sha256(relevant_file.encode('utf-8')).hexdigest()
if relevant_line_start == -1: if relevant_line_start == -1:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}" link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}"
elif relevant_line_end: 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}" link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}-R{relevant_line_end}"
else: else:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}" link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}"
return link return link
@ -528,3 +721,34 @@ class GithubProvider(GitProvider):
return pr_id return pr_id
except: except:
return "" return ""
def auto_approve(self) -> bool:
try:
res = self.pr.create_review(event="APPROVE")
if res.state == "APPROVED":
return True
return False
except Exception as e:
get_logger().exception(f"Failed to auto-approve, error: {e}")
return False
def calc_pr_statistics(self, pull_request_data: dict):
try:
out = {}
from datetime import datetime
created_at = pull_request_data['created_at']
closed_at = pull_request_data['closed_at']
closed_at_datetime = datetime.strptime(closed_at, "%Y-%m-%dT%H:%M:%SZ")
created_at_datetime = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ")
difference = closed_at_datetime - created_at_datetime
out['hours'] = difference.total_seconds() / 3600
out['commits'] = pull_request_data['commits']
out['comments'] = pull_request_data['comments']
out['review_comments'] = pull_request_data['review_comments']
out['changed_files'] = pull_request_data['changed_files']
out['additions'] = pull_request_data['additions']
out['deletions'] = pull_request_data['deletions']
except Exception as e:
get_logger().exception(f"Failed to calculate PR statistics, error: {e}")
return {}
return out

View File

@ -7,10 +7,10 @@ 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 find_line_number_of_relevant_line_in_file from ..algo.utils import load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file
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 GitProvider
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from ..log import get_logger from ..log import get_logger
@ -63,7 +63,7 @@ class GitLabProvider(GitProvider):
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
def _get_pr_file_content(self, file_path: str, branch: str) -> str: def get_pr_file_content(self, file_path: str, branch: str) -> str:
try: try:
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode() return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
except GitlabGetError: except GitlabGetError:
@ -88,10 +88,8 @@ class GitLabProvider(GitProvider):
diff_files = [] diff_files = []
for diff in diffs: for diff in diffs:
if is_valid_file(diff['new_path']): if is_valid_file(diff['new_path']):
# original_file_content_str = self._get_pr_file_content(diff['old_path'], self.mr.target_branch) original_file_content_str = self.get_pr_file_content(diff['old_path'], self.mr.diff_refs['base_sha'])
# new_file_content_str = self._get_pr_file_content(diff['new_path'], self.mr.source_branch) new_file_content_str = self.get_pr_file_content(diff['new_path'], self.mr.diff_refs['head_sha'])
original_file_content_str = self._get_pr_file_content(diff['old_path'], self.mr.diff_refs['base_sha'])
new_file_content_str = self._get_pr_file_content(diff['new_path'], self.mr.diff_refs['head_sha'])
try: try:
if isinstance(original_file_content_str, bytes): if isinstance(original_file_content_str, bytes):
@ -151,21 +149,26 @@ class GitLabProvider(GitProvider):
def get_comment_url(self, comment): def get_comment_url(self, comment):
return f"{self.mr.web_url}#note_{comment.id}" return f"{self.mr.web_url}#note_{comment.id}"
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True): def publish_persistent_comment(self, pr_comment: str,
initial_header: str,
update_header: bool = True,
name='review',
final_update_message=True):
try: try:
for comment in self.mr.notes.list(get_all=True)[::-1]: for comment in self.mr.notes.list(get_all=True)[::-1]:
if comment.body.startswith(initial_header): if comment.body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url() latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment) comment_url = self.get_comment_url(comment)
if update_header: if update_header:
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n" updated_header = f"{initial_header}\n\n### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header) pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else: else:
pr_comment_updated = pr_comment pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message") get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
response = self.mr.notes.update(comment.id, {'body': pr_comment_updated}) response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
self.publish_comment( if final_update_message:
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}") self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return return
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}") get_logger().exception(f"Failed to update persistent review, error: {e}")
@ -176,6 +179,14 @@ class GitLabProvider(GitProvider):
comment = self.mr.notes.create({'body': mr_comment}) comment = self.mr.notes.create({'body': mr_comment})
if is_temporary: if is_temporary:
self.temp_comments.append(comment) self.temp_comments.append(comment)
return comment
def edit_comment(self, comment, body: str):
self.mr.notes.update(comment.id,{'body': body} )
def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
discussion = self.mr.discussions.get(comment_id)
discussion.notes.create({'body': body})
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):
edit_type, found, source_line_no, target_file, target_line_no = self.search_line(relevant_file, edit_type, found, source_line_no, target_file, target_line_no = self.search_line(relevant_file,
@ -183,7 +194,7 @@ 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)
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, absolute_position: int = None):
raise NotImplementedError("Gitlab provider does not support creating inline comments yet") raise NotImplementedError("Gitlab provider does not support creating inline comments yet")
def create_inline_comments(self, comments: list[dict]): def create_inline_comments(self, comments: list[dict]):
@ -211,7 +222,11 @@ class GitLabProvider(GitProvider):
pos_obj['new_line'] = target_line_no - 1 pos_obj['new_line'] = target_line_no - 1
pos_obj['old_line'] = source_line_no - 1 pos_obj['old_line'] = source_line_no - 1
get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}")
self.mr.discussions.create({'body': body, 'position': pos_obj}) try:
self.mr.discussions.create({'body': body, 'position': pos_obj})
except Exception as e:
get_logger().debug(
f"Failed to create comment in {self.id_mr} with position {pos_obj} (probably not a '+' line)")
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]:
changes = self.mr.changes() # Retrieve the changes for the merge request once changes = self.mr.changes() # Retrieve the changes for the merge request once
@ -356,7 +371,7 @@ class GitLabProvider(GitProvider):
except Exception: except Exception:
return "" return ""
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
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:
@ -404,9 +419,12 @@ class GitLabProvider(GitProvider):
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
pass pass
def get_labels(self): def get_pr_labels(self):
return self.mr.labels return self.mr.labels
def get_repo_labels(self):
return self.gl.projects.get(self.id_project).labels.list()
def get_commit_messages(self): def get_commit_messages(self):
""" """
Retrieves the commit messages of a pull request. Retrieves the commit messages of a pull request.
@ -433,18 +451,18 @@ class GitLabProvider(GitProvider):
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str: def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
if relevant_line_start == -1: if relevant_line_start == -1:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads" link = f"{self.gl.url}/{self.id_project}/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads"
elif relevant_line_end: 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}" link = f"{self.gl.url}/{self.id_project}/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}-L{relevant_line_end}"
else: else:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}" link = f"{self.gl.url}/{self.id_project}/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}"
return link return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant_file'].strip('`').strip("'").rstrip()
relevant_line_str = suggestion['relevant line'] relevant_line_str = suggestion['relevant_line'].rstrip()
if not relevant_line_str: if not relevant_line_str:
return "" return ""
@ -453,7 +471,7 @@ class GitLabProvider(GitProvider):
if absolute_position != -1: if absolute_position != -1:
# link to right file only # 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 = f"{self.gl.url}/{self.id_project}/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{absolute_position}"
# # link to diff # # link to diff
# sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest() # sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest()

View File

@ -5,7 +5,8 @@ from typing import List
from git import Repo 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 GitProvider
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -121,9 +122,6 @@ class LocalGitProvider(GitProvider):
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):
raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') raise NotImplementedError('Publishing inline comments is not implemented for the local git 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 local git provider')
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') raise NotImplementedError('Publishing inline comments is not implemented for the local git provider')
@ -178,5 +176,5 @@ class LocalGitProvider(GitProvider):
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError('Getting issue comments is not implemented for the local git provider') raise NotImplementedError('Getting issue comments is not implemented for the local git provider')
def get_labels(self): def get_pr_labels(self):
raise NotImplementedError('Getting labels is not implemented for the local git provider') raise NotImplementedError('Getting labels is not implemented for the local git provider')

View File

@ -7,14 +7,26 @@ from dynaconf import Dynaconf
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 from pr_agent.log import get_logger
from starlette_context import context
def apply_repo_settings(pr_url): def apply_repo_settings(pr_url):
if get_settings().config.use_repo_settings_file: if get_settings().config.use_repo_settings_file:
repo_settings_file = None repo_settings_file = None
try: try:
git_provider = get_git_provider()(pr_url) try:
repo_settings = git_provider.get_repo_settings() repo_settings = context.get("repo_settings", None)
except Exception:
repo_settings = None
pass
if repo_settings is None: # None is different from "", which is a valid value
git_provider = get_git_provider()(pr_url)
repo_settings = git_provider.get_repo_settings()
try:
context["repo_settings"] = repo_settings
except Exception:
pass
if repo_settings: if repo_settings:
repo_settings_file = None repo_settings_file = None
fd, repo_settings_file = tempfile.mkstemp(suffix='.toml') fd, repo_settings_file = tempfile.mkstemp(suffix='.toml')
@ -26,7 +38,7 @@ def apply_repo_settings(pr_url):
section_dict[key] = value section_dict[key] = value
get_settings().unset(section) get_settings().unset(section)
get_settings().set(section, section_dict, merge=False) get_settings().set(section, section_dict, merge=False)
get_logger().info(f"Applying repo settings for section {section}, contents: {contents}") get_logger().info(f"Applying repo settings:\n{new_settings.as_dict()}")
except Exception as e: except Exception as e:
get_logger().exception("Failed to apply repo settings", e) get_logger().exception("Failed to apply repo settings", e)
finally: finally:

View File

@ -0,0 +1,13 @@
from pr_agent.config_loader import get_settings
from pr_agent.identity_providers.default_identity_provider import DefaultIdentityProvider
_IDENTITY_PROVIDERS = {
'default': DefaultIdentityProvider
}
def get_identity_provider():
identity_provider_id = get_settings().get("CONFIG.IDENTITY_PROVIDER", "default")
if identity_provider_id not in _IDENTITY_PROVIDERS:
raise ValueError(f"Unknown identity provider: {identity_provider_id}")
return _IDENTITY_PROVIDERS[identity_provider_id]()

View File

@ -0,0 +1,9 @@
from pr_agent.identity_providers.identity_provider import Eligibility, IdentityProvider
class DefaultIdentityProvider(IdentityProvider):
def verify_eligibility(self, git_provider, git_provider_id, pr_url):
return Eligibility.ELIGIBLE
def inc_invocation_count(self, git_provider, git_provider_id):
pass

View File

@ -0,0 +1,18 @@
from abc import ABC, abstractmethod
from enum import Enum
class Eligibility(Enum):
NOT_ELIGIBLE = 0
ELIGIBLE = 1
TRIAL = 2
class IdentityProvider(ABC):
@abstractmethod
def verify_eligibility(self, git_provider, git_provier_id, pr_url):
pass
@abstractmethod
def inc_invocation_count(self, git_provider, git_provider_id):
pass

View File

@ -1,10 +1,13 @@
import json import json
import logging import logging
import os
import sys import sys
from enum import Enum from enum import Enum
from loguru import logger from loguru import logger
from pr_agent.config_loader import get_settings
class LoggingFormat(str, Enum): class LoggingFormat(str, Enum):
CONSOLE = "CONSOLE" CONSOLE = "CONSOLE"
@ -15,23 +18,45 @@ def json_format(record: dict) -> str:
return record["message"] return record["message"]
def analytics_filter(record: dict) -> bool:
return record.get("extra", {}).get("analytics", False)
def inv_analytics_filter(record: dict) -> bool:
return not record.get("extra", {}).get("analytics", False)
def setup_logger(level: str = "INFO", fmt: LoggingFormat = LoggingFormat.CONSOLE): def setup_logger(level: str = "INFO", fmt: LoggingFormat = LoggingFormat.CONSOLE):
level: int = logging.getLevelName(level.upper()) level: int = logging.getLevelName(level.upper())
if type(level) is not int: if type(level) is not int:
level = logging.INFO level = logging.INFO
if fmt == LoggingFormat.JSON: if fmt == LoggingFormat.JSON and os.getenv("LOG_SANE", "0").lower() == "0": # better debugging github_app
logger.remove(None) logger.remove(None)
logger.add( logger.add(
sys.stdout, sys.stdout,
filter=inv_analytics_filter,
level=level, level=level,
format="{message}", format="{message}",
colorize=False, colorize=False,
serialize=True, serialize=True,
) )
elif fmt == LoggingFormat.CONSOLE: elif fmt == LoggingFormat.CONSOLE: # does not print the 'extra' fields
logger.remove(None) logger.remove(None)
logger.add(sys.stdout, level=level, colorize=True) logger.add(sys.stdout, level=level, colorize=True, filter=inv_analytics_filter)
log_folder = get_settings().get("CONFIG.ANALYTICS_FOLDER", "")
if log_folder:
pid = os.getpid()
log_file = os.path.join(log_folder, f"pr-agent.{pid}.log")
logger.add(
log_file,
filter=analytics_filter,
level=level,
format="{message}",
colorize=False,
serialize=True,
)
return logger return logger

View File

@ -2,15 +2,18 @@ from pr_agent.config_loader import get_settings
def get_secret_provider(): def get_secret_provider():
try: if not get_settings().get("CONFIG.SECRET_PROVIDER"):
provider_id = get_settings().config.secret_provider return None
except AttributeError as e:
raise ValueError("secret_provider is a required attribute in the configuration file") from e provider_id = get_settings().config.secret_provider
try: if provider_id == 'google_cloud_storage':
if provider_id == 'google_cloud_storage': try:
from pr_agent.secret_providers.google_cloud_storage_secret_provider import GoogleCloudStorageSecretProvider from pr_agent.secret_providers.google_cloud_storage_secret_provider import GoogleCloudStorageSecretProvider
return GoogleCloudStorageSecretProvider() return GoogleCloudStorageSecretProvider()
else: except Exception as e:
raise ValueError(f"Unknown secret provider: {provider_id}") raise ValueError(f"Failed to initialize google_cloud_storage secret provider {provider_id}") from e
except Exception as e: else:
raise ValueError(f"Failed to initialize secret provider {provider_id}") from e raise ValueError("Unknown SECRET_PROVIDER")

View File

@ -0,0 +1,139 @@
# This file contains the code for the Azure DevOps Server webhook server.
# The server listens for incoming webhooks from Azure DevOps Server and forwards them to the PR Agent.
# ADO webhook documentation: https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops
import json
import os
import re
import secrets
from urllib.parse import unquote
import uvicorn
from fastapi import APIRouter, Depends, FastAPI, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials
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, command2class
from pr_agent.algo.utils import update_settings_from_args
from pr_agent.config_loader import get_settings
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import get_logger
from fastapi import Request, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pr_agent.log import get_logger
security = HTTPBasic()
router = APIRouter()
available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*")
azure_devops_server = get_settings().get("azure_devops_server")
WEBHOOK_USERNAME = azure_devops_server.get("webhook_username")
WEBHOOK_PASSWORD = azure_devops_server.get("webhook_password")
def handle_request(
background_tasks: BackgroundTasks, url: str, body: str, log_context: dict
):
log_context["action"] = body
log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body)
# currently only basic auth is supported with azure webhooks
# for this reason, https must be enabled to ensure the credentials are not sent in clear text
def authorize(credentials: HTTPBasicCredentials = Depends(security)):
is_user_ok = secrets.compare_digest(credentials.username, WEBHOOK_USERNAME)
is_pass_ok = secrets.compare_digest(credentials.password, WEBHOOK_PASSWORD)
if not (is_user_ok and is_pass_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect username or password.',
headers={'WWW-Authenticate': 'Basic'},
)
async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
apply_repo_settings(api_url)
commands = get_settings().get(f"azure_devops_server.{commands_conf}")
for command in commands:
try:
split_command = command.split(" ")
command = split_command[0]
args = split_command[1:]
other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, new_command)
except Exception as e:
get_logger().error(f"Failed to perform command {command}: {e}")
@router.post("/", dependencies=[Depends(authorize)])
async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "azure_devops_server"}
data = await request.json()
get_logger().info(json.dumps(data))
actions = []
if data["eventType"] == "git.pullrequest.created":
# API V1 (latest)
pr_url = unquote(data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git"))
log_context["event"] = data["eventType"]
log_context["api_url"] = pr_url
await _perform_commands_azure("pr_commands", PRAgent(), pr_url, log_context)
return
elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event" and "content" in data["resource"]["comment"]:
if available_commands_rgx.match(data["resource"]["comment"]["content"]):
if(data["resourceVersion"] == "2.0"):
repo = data["resource"]["pullRequest"]["repository"]["webUrl"]
pr_url = unquote(f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}')
actions = [data["resource"]["comment"]["content"]]
else:
# API V1 not supported as it does not contain the PR URL
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=json.dumps({"message": "version 1.0 webhook for Azure Devops PR comment is not supported. please upgrade to version 2.0"})),
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=json.dumps({"message": "Unsupported command"}),
)
else:
return JSONResponse(
status_code=status.HTTP_204_NO_CONTENT,
content=json.dumps({"message": "Unsupported event"}),
)
log_context["event"] = data["eventType"]
log_context["api_url"] = pr_url
for action in actions:
try:
handle_request(background_tasks, pr_url, action, log_context)
except Exception as e:
get_logger().error("Azure DevOps Trigger failed. Error:" + str(e))
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=json.dumps({"message": "Internal server error"}),
)
return JSONResponse(
status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggerd successfully"})
)
@router.get("/")
async def root():
return {"status": "ok"}
def start():
app = FastAPI(middleware=[Middleware(RawContextMiddleware)])
app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
if __name__ == "__main__":
start()

View File

@ -1,3 +1,4 @@
import base64
import copy import copy
import hashlib import hashlib
import json import json
@ -15,13 +16,22 @@ from starlette_context import context
from starlette_context.middleware import RawContextMiddleware 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.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.utils import apply_repo_settings
from pr_agent.identity_providers import get_identity_provider
from pr_agent.identity_providers.identity_provider import Eligibility
from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider from pr_agent.secret_providers import get_secret_provider
from pr_agent.servers.github_action_runner import get_setting_or_env, is_true
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer
setup_logger(fmt=LoggingFormat.JSON) setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
router = APIRouter() router = APIRouter()
secret_provider = get_secret_provider() secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
async def get_bearer_token(shared_secret: str, client_key: str): async def get_bearer_token(shared_secret: str, client_key: str):
try: try:
@ -63,6 +73,24 @@ async def handle_manifest(request: Request, response: Response):
manifest_obj = json.loads(manifest) manifest_obj = json.loads(manifest)
return JSONResponse(manifest_obj) return JSONResponse(manifest_obj)
async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
apply_repo_settings(api_url)
commands = get_settings().get(f"bitbucket_app.{commands_conf}", {})
for command in commands:
try:
split_command = command.split(" ")
command = split_command[0]
args = split_command[1:]
other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, new_command)
except Exception as e:
get_logger().error(f"Failed to perform command {command}: {e}")
@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):
log_context = {"server_type": "bitbucket_app"} log_context = {"server_type": "bitbucket_app"}
@ -74,11 +102,27 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
get_logger().debug(data) get_logger().debug(data)
async def inner(): async def inner():
try: try:
owner = data["data"]["repository"]["owner"]["username"] try:
if data["data"]["actor"]["type"] != "user":
return "OK"
except KeyError:
get_logger().error("Failed to get actor type, check previous logs, this shouldn't happen.")
try:
owner = data["data"]["repository"]["owner"]["username"]
except Exception as e:
get_logger().error(f"Failed to get owner, will continue: {e}")
owner = "unknown"
sender_id = data["data"]["actor"]["account_id"]
log_context["sender"] = owner log_context["sender"] = owner
secrets = json.loads(secret_provider.get_secret(owner)) log_context["sender_id"] = sender_id
jwt_parts = input_jwt.split(".")
claim_part = jwt_parts[1]
claim_part += "=" * (-len(claim_part) % 4)
decoded_claims = base64.urlsafe_b64decode(claim_part)
claims = json.loads(decoded_claims)
client_key = claims["iss"]
secrets = json.loads(secret_provider.get_secret(client_key))
shared_secret = secrets["shared_secret"] shared_secret = secrets["shared_secret"]
client_key = secrets["client_key"]
jwt.decode(input_jwt, shared_secret, audience=client_key, algorithms=["HS256"]) jwt.decode(input_jwt, shared_secret, audience=client_key, algorithms=["HS256"])
bearer_token = await get_bearer_token(shared_secret, client_key) bearer_token = await get_bearer_token(shared_secret, client_key)
context['bitbucket_bearer_token'] = bearer_token context['bitbucket_bearer_token'] = bearer_token
@ -89,15 +133,32 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "pull_request" log_context["event"] = "pull_request"
with get_logger().contextualize(**log_context): if pr_url:
await agent.handle_request(pr_url, "review") with get_logger().contextualize(**log_context):
apply_repo_settings(pr_url)
if get_identity_provider().verify_eligibility("bitbucket",
sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE:
if get_settings().get("bitbucket_app.pr_commands"):
await _perform_commands_bitbucket("pr_commands", PRAgent(), pr_url, log_context)
else: # backwards compatibility
auto_review = get_setting_or_env("BITBUCKET_APP.AUTO_REVIEW", None)
if is_true(auto_review): # by default, auto review is disabled
await PRReviewer(pr_url).run()
auto_improve = get_setting_or_env("BITBUCKET_APP.AUTO_IMPROVE", None)
if is_true(auto_improve): # by default, auto improve is disabled
await PRCodeSuggestions(pr_url).run()
auto_describe = get_setting_or_env("BITBUCKET_APP.AUTO_DESCRIBE", None)
if is_true(auto_describe): # by default, auto describe is disabled
await PRDescription(pr_url).run()
elif event == "pullrequest:comment_created": elif event == "pullrequest:comment_created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "comment" log_context["event"] = "comment"
comment_body = data["data"]["comment"]["content"]["raw"] comment_body = data["data"]["comment"]["content"]["raw"]
with get_logger().contextualize(**log_context): with get_logger().contextualize(**log_context):
await agent.handle_request(pr_url, comment_body) if get_identity_provider().verify_eligibility("bitbucket",
sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE:
await agent.handle_request(pr_url, comment_body)
except Exception as e: except Exception as e:
get_logger().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)

View File

@ -1,4 +1,5 @@
import json import json
import os
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
@ -13,35 +14,55 @@ 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
from pr_agent.log import get_logger from pr_agent.log import get_logger
from pr_agent.servers.utils import verify_signature
router = APIRouter() router = APIRouter()
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict): def handle_request(
background_tasks: BackgroundTasks, url: str, body: str, log_context: dict
):
log_context["action"] = body log_context["action"] = body
log_context["event"] = "pull_request" if body == "review" else "comment"
log_context["api_url"] = url log_context["api_url"] = url
with get_logger().contextualize(**log_context): with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body) background_tasks.add_task(PRAgent().handle_request, url, body)
@router.post("/webhook") @router.post("/")
async def handle_webhook(background_tasks: BackgroundTasks, request: Request): async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "bitbucket_server"} log_context = {"server_type": "bitbucket_server"}
data = await request.json() data = await request.json()
get_logger().info(json.dumps(data)) get_logger().info(json.dumps(data))
pr_id = data['pullRequest']['id'] webhook_secret = get_settings().get("BITBUCKET_SERVER.WEBHOOK_SECRET", None)
repository_name = data['pullRequest']['toRef']['repository']['slug'] if webhook_secret:
project_name = data['pullRequest']['toRef']['repository']['project']['key'] body_bytes = await request.body()
signature_header = request.headers.get("x-hub-signature", None)
verify_signature(body_bytes, webhook_secret, signature_header)
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") bitbucket_server = get_settings().get("BITBUCKET_SERVER.URL")
pr_url = f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}" pr_url = f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}"
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "pull_request" log_context["event"] = "pull_request"
handle_request(background_tasks, pr_url, "review", log_context) if data["eventKey"] == "pr:opened":
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"})) body = "review"
elif data["eventKey"] == "pr:comment:added":
body = data["comment"]["text"]
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=json.dumps({"message": "Unsupported event"}),
)
handle_request(background_tasks, pr_url, body, log_context)
return JSONResponse(
status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"})
)
@router.get("/") @router.get("/")
@ -50,15 +71,10 @@ async def root():
def start(): def start():
bitbucket_server_url = get_settings().get("BITBUCKET_SERVER.URL", None) app = FastAPI(middleware=[Middleware(RawContextMiddleware)])
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) app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=3000) uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
if __name__ == '__main__': if __name__ == "__main__":
start() start()

View File

@ -8,6 +8,7 @@ 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.git_providers.utils import apply_repo_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
from pr_agent.servers.github_app import handle_line_comments
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
@ -79,38 +80,61 @@ async def run_action():
# 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", "ready_for_review", "review_requested"]:
pr_url = event_payload.get("pull_request", {}).get("url") pr_url = event_payload.get("pull_request", {}).get("url")
if pr_url: if pr_url:
# legacy - supporting both GITHUB_ACTION and GITHUB_ACTION_CONFIG
auto_review = get_setting_or_env("GITHUB_ACTION.AUTO_REVIEW", None) auto_review = get_setting_or_env("GITHUB_ACTION.AUTO_REVIEW", None)
if auto_review is None:
auto_review = get_setting_or_env("GITHUB_ACTION_CONFIG.AUTO_REVIEW", None)
auto_describe = get_setting_or_env("GITHUB_ACTION.AUTO_DESCRIBE", None)
if auto_describe is None:
auto_describe = get_setting_or_env("GITHUB_ACTION_CONFIG.AUTO_DESCRIBE", None)
auto_improve = get_setting_or_env("GITHUB_ACTION.AUTO_IMPROVE", None)
if auto_improve is None:
auto_improve = get_setting_or_env("GITHUB_ACTION_CONFIG.AUTO_IMPROVE", None)
# invoke by default all three tools
if auto_describe is None or is_true(auto_describe):
await PRDescription(pr_url).run()
if auto_review is None or is_true(auto_review): 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 auto_improve is None or is_true(auto_improve):
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() 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" or GITHUB_EVENT_NAME == "pull_request_review_comment":
action = event_payload.get("action") action = event_payload.get("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")
try:
if GITHUB_EVENT_NAME == "pull_request_review_comment":
if '/ask' in comment_body:
comment_body = handle_line_comments(event_payload, comment_body)
except Exception as e:
get_logger().error(f"Failed to handle line comments: {e}")
return
if comment_body: if comment_body:
is_pr = False is_pr = False
disable_eyes = False
# check if issue is pull request # check if issue is pull request
if event_payload.get("issue", {}).get("pull_request"): if event_payload.get("issue", {}).get("pull_request"):
url = event_payload.get("issue", {}).get("pull_request", {}).get("url") url = event_payload.get("issue", {}).get("pull_request", {}).get("url")
is_pr = True is_pr = True
elif event_payload.get("comment", {}).get("pull_request_url"): # for 'pull_request_review_comment
url = event_payload.get("comment", {}).get("pull_request_url")
is_pr = True
disable_eyes = True
else: else:
url = event_payload.get("issue", {}).get("url") url = event_payload.get("issue", {}).get("url")
if 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=url) provider = get_git_provider()(pr_url=url)
if is_pr: if is_pr:
await PRAgent().handle_request(url, body, notify=lambda: provider.add_eyes_reaction(comment_id)) await PRAgent().handle_request(url, body,
notify=lambda: provider.add_eyes_reaction(comment_id, disable_eyes=disable_eyes))
else: else:
await PRAgent().handle_request(url, body) await PRAgent().handle_request(url, body)

View File

@ -1,7 +1,9 @@
import asyncio.locks
import copy import copy
import os import os
import asyncio.locks import re
from typing import Any, Dict, List, Tuple import uuid
from typing import Any, Dict, Tuple
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
@ -13,13 +15,21 @@ 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.git_providers.utils import apply_repo_settings
from pr_agent.git_providers.git_provider import IncrementalPR from pr_agent.git_providers.git_provider import IncrementalPR
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.identity_providers import get_identity_provider
from pr_agent.identity_providers.identity_provider import Eligibility
from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.servers.utils import verify_signature, DefaultDictWithTimeout from pr_agent.servers.utils import DefaultDictWithTimeout, verify_signature
setup_logger(fmt=LoggingFormat.JSON)
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
base_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
build_number_path = os.path.join(base_path, "build_number.txt")
if os.path.exists(build_number_path):
with open(build_number_path) as f:
build_number = f.read().strip()
else:
build_number = "unknown"
router = APIRouter() router = APIRouter()
@ -34,7 +44,6 @@ async def handle_github_webhooks(request: Request, response: Response):
body = await get_body(request) body = await get_body(request)
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)
@ -63,10 +72,172 @@ async def get_body(request):
return body return body
_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) _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) _pending_task_duplicate_push_conditions = DefaultDictWithTimeout(asyncio.locks.Condition, ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
async def handle_comments_on_pr(body: Dict[str, Any],
event: str,
sender: str,
sender_id: str,
action: str,
log_context: Dict[str, Any],
agent: PRAgent):
if "comment" not in body:
return {}
comment_body = body.get("comment", {}).get("body")
if comment_body and isinstance(comment_body, str) and not comment_body.lstrip().startswith("/"):
get_logger().info("Ignoring comment not starting with /")
return {}
disable_eyes = False
if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
api_url = body["issue"]["pull_request"]["url"]
elif "comment" in body and "pull_request_url" in body["comment"]:
api_url = body["comment"]["pull_request_url"]
try:
if ('/ask' in comment_body and
'subject_type' in body["comment"] and body["comment"]["subject_type"] == "line"):
# comment on a code line in the "files changed" tab
comment_body = handle_line_comments(body, comment_body)
disable_eyes = True
except Exception as e:
get_logger().error(f"Failed to handle line comments: {e}")
else:
return {}
log_context["api_url"] = api_url
comment_id = body.get("comment", {}).get("id")
provider = get_git_provider()(pr_url=api_url)
with get_logger().contextualize(**log_context):
if get_identity_provider().verify_eligibility("github", sender_id, api_url) is not Eligibility.NOT_ELIGIBLE:
get_logger().info(f"Processing comment on PR {api_url=}, comment_body={comment_body}")
await agent.handle_request(api_url, comment_body,
notify=lambda: provider.add_eyes_reaction(comment_id, disable_eyes=disable_eyes))
else:
get_logger().info(f"User {sender=} is not eligible to process comment on PR {api_url=}")
async def handle_new_pr_opened(body: Dict[str, Any],
event: str,
sender: str,
sender_id: str,
action: str,
log_context: Dict[str, Any],
agent: PRAgent):
title = body.get("pull_request", {}).get("title", "")
# logic to ignore PRs with specific titles (e.g. "[Auto] ...")
ignore_pr_title_re = get_settings().get("GITHUB_APP.IGNORE_PR_TITLE", [])
if not isinstance(ignore_pr_title_re, list):
ignore_pr_title_re = [ignore_pr_title_re]
if ignore_pr_title_re and any(re.search(regex, title) for regex in ignore_pr_title_re):
get_logger().info(f"Ignoring PR with title '{title}' due to github_app.ignore_pr_title setting")
return {}
pull_request, api_url = _check_pull_request_event(action, body, log_context)
if not (pull_request and api_url):
get_logger().info(f"Invalid PR event: {action=} {api_url=}")
return {}
if action in get_settings().github_app.handle_pr_actions: # ['opened', 'reopened', 'ready_for_review', 'review_requested']
if get_identity_provider().verify_eligibility("github", sender_id, api_url) is not Eligibility.NOT_ELIGIBLE:
await _perform_auto_commands_github("pr_commands", agent, body, api_url, log_context)
else:
get_logger().info(f"User {sender=} is not eligible to process PR {api_url=}")
async def handle_push_trigger_for_new_commits(body: Dict[str, Any],
event: str,
sender: str,
sender_id: str,
action: str,
log_context: Dict[str, Any],
agent: PRAgent):
pull_request, api_url = _check_pull_request_event(action, body, log_context)
if not (pull_request and api_url):
return {}
apply_repo_settings(api_url) # we need to apply the repo settings to get the correct settings for the PR. This is quite expensive - a call to the git provider is made for each PR event.
if not get_settings().github_app.handle_push_trigger:
return {}
# TODO: do we still want to get the list of commits to filter bot/merge commits?
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 {}
# 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 {}
if get_identity_provider().verify_eligibility("github", sender_id, api_url) is not Eligibility.NOT_ELIGIBLE:
get_logger().info(f"Performing incremental review for {api_url=} because of {event=} and {action=}")
await _perform_auto_commands_github("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
def handle_closed_pr(body, event, action, log_context):
pull_request = body.get("pull_request", {})
is_merged = pull_request.get("merged", False)
if not is_merged:
return
api_url = pull_request.get("url", "")
pr_statistics = get_git_provider()(pr_url=api_url).calc_pr_statistics(pull_request)
log_context["api_url"] = api_url
get_logger().info("PR-Agent statistics for closed PR", analytics=True, pr_statistics=pr_statistics, **log_context)
def get_log_context(body, event, action, build_number):
sender = ""
sender_id = ""
try:
sender = body.get("sender", {}).get("login")
sender_id = body.get("sender", {}).get("id")
repo = body.get("repository", {}).get("full_name", "")
git_org = body.get("organization", {}).get("login", "")
app_name = get_settings().get("CONFIG.APP_NAME", "Unknown")
log_context = {"action": action, "event": event, "sender": sender, "server_type": "github_app",
"request_id": uuid.uuid4().hex, "build_number": build_number, "app_name": app_name,
"repo": repo, "git_org": git_org}
except Exception as e:
get_logger().error("Failed to get log context", e)
log_context = {}
return log_context, sender, sender_id
async def handle_request(body: Dict[str, Any], event: str): async def handle_request(body: Dict[str, Any], event: str):
""" """
@ -74,119 +245,52 @@ async def handle_request(body: Dict[str, Any], event: str):
Args: Args:
body: The request body. body: The request body.
event: The GitHub event type. event: The GitHub event type (e.g. "pull_request", "issue_comment", etc.).
""" """
action = body.get("action") action = body.get("action") # "created", "opened", "reopened", "ready_for_review", "review_requested", "synchronize"
if not action: if not action:
return {} return {}
agent = PRAgent() agent = PRAgent()
bot_user = get_settings().github_app.bot_user log_context, sender, sender_id = get_log_context(body, event, action, build_number)
sender = body.get("sender", {}).get("login")
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): # handle comments on PRs
return {}
# handle all sorts of comment events (e.g. issue_comment)
if action == 'created': if action == 'created':
if "comment" not in body: get_logger().debug(f'Request body', artifact=body, event=event)
return {} await handle_comments_on_pr(body, event, sender, sender_id, action, log_context, agent)
comment_body = body.get("comment", {}).get("body") # handle new PRs
if sender and bot_user in sender: elif event == 'pull_request' and action != 'synchronize' and action != 'closed':
get_logger().info(f"Ignoring comment from {bot_user} user") get_logger().debug(f'Request body', artifact=body, event=event)
return {} await handle_new_pr_opened(body, event, sender, sender_id, action, log_context, agent)
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"]:
api_url = body["issue"]["pull_request"]["url"]
elif "comment" in body and "pull_request_url" in body["comment"]:
api_url = body["comment"]["pull_request_url"]
else:
return {}
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")
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))
# handle pull_request event:
# 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
elif event == 'pull_request' and action != 'synchronize':
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
if not (pull_request and api_url):
return {}
if action in get_settings().github_app.handle_pr_actions:
if action == "review_requested":
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
return {}
get_logger().info(f"Performing review for {api_url=} because of {event=} and {action=}")
await _perform_commands("pr_commands", agent, body, api_url, log_context)
# handle pull_request event with synchronize action - "push trigger" for new commits # handle pull_request event with synchronize action - "push trigger" for new commits
elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger: elif event == 'pull_request' and action == 'synchronize':
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user) get_logger().debug(f'Request body', artifact=body, event=event)
if not (pull_request and api_url): await handle_push_trigger_for_new_commits(body, event, sender, sender_id, action, log_context, agent)
return {} elif event == 'pull_request' and action == 'closed':
if get_settings().get("CONFIG.ANALYTICS_FOLDER", ""):
# TODO: do we still want to get the list of commits to filter bot/merge commits? handle_closed_pr(body, event, action, log_context)
before_sha = body.get("before") else:
after_sha = body.get("after") get_logger().info(f"event {event=} action {action=} does not require any handling")
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 {} return {}
def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_user: str) -> Tuple[Dict[str, Any], str]: def handle_line_comments(body: Dict, comment_body: [str, Any]) -> str:
if not comment_body:
return ""
start_line = body["comment"]["start_line"]
end_line = body["comment"]["line"]
start_line = end_line if not start_line else start_line
question = comment_body.replace('/ask', '').strip()
diff_hunk = body["comment"]["diff_hunk"]
get_settings().set("ask_diff_hunk", diff_hunk)
path = body["comment"]["path"]
side = body["comment"]["side"]
comment_id = body["comment"]["id"]
if '/ask' in comment_body:
comment_body = f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={comment_id} {question}"
return comment_body
def _check_pull_request_event(action: str, body: dict, log_context: dict) -> Tuple[Dict[str, Any], str]:
invalid_result = {}, "" invalid_result = {}, ""
pull_request = body.get("pull_request") pull_request = body.get("pull_request")
if not pull_request: if not pull_request:
@ -195,7 +299,7 @@ def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_us
if not api_url: if not api_url:
return invalid_result return invalid_result
log_context["api_url"] = api_url 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: if pull_request.get("draft", True) or pull_request.get("state") != "open":
return invalid_result return invalid_result
if action in ("review_requested", "synchronize") and pull_request.get("created_at") == pull_request.get("updated_at"): 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 # avoid double reviews when opening a PR for the first time
@ -203,51 +307,40 @@ def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_us
return pull_request, api_url return pull_request, api_url
async def _perform_commands(commands_conf: str, agent: PRAgent, body: dict, api_url: str, log_context: dict): async def _perform_auto_commands_github(commands_conf: str, agent: PRAgent, body: dict, api_url: str, log_context: dict):
apply_repo_settings(api_url) apply_repo_settings(api_url)
commands = get_settings().get(f"github_app.{commands_conf}") commands = get_settings().get(f"github_app.{commands_conf}")
if not commands:
with get_logger().contextualize(**log_context):
get_logger().info(f"New PR, but no auto commands configured")
return
for command in commands: 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)
get_logger().info(body)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context): with get_logger().contextualize(**log_context):
get_logger().info(f"{commands_conf}. Performing auto command '{new_command}', for {api_url=}")
await agent.handle_request(api_url, new_command) await agent.handle_request(api_url, new_command)
def _is_duplicate_request(body: Dict[str, Any]) -> bool:
"""
In some deployments its possible to get duplicate requests if the handling is long,
This function checks if the request is duplicate and if so - ignores it.
"""
request_hash = hash(str(body))
get_logger().info(f"request_hash: {request_hash}")
is_duplicate = _duplicate_requests_cache.get(request_hash, False)
_duplicate_requests_cache[request_hash] = True
if is_duplicate:
get_logger().info(f"Ignoring duplicate request {request_hash}")
return is_duplicate
@router.get("/") @router.get("/")
async def root(): async def root():
return {"status": "ok"} return {"status": "ok"}
if get_settings().github_app.override_deployment_type:
# Override the deployment type to app
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app")
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
def start(): def start():
if get_settings().github_app.override_deployment_type:
# Override the deployment type to app
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app")
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000"))) uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
if __name__ == '__main__': if __name__ == '__main__':
start() start()

View File

@ -7,9 +7,8 @@ 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.log import LoggingFormat, get_logger, setup_logger
from pr_agent.servers.help import bot_help_text
setup_logger(fmt=LoggingFormat.JSON) setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
NOTIFICATION_URL = "https://api.github.com/notifications" NOTIFICATION_URL = "https://api.github.com/notifications"
@ -104,8 +103,6 @@ async def polling_loop():
notify=lambda: git_provider.add_eyes_reaction(comment_id)) # noqa E501 notify=lambda: git_provider.add_eyes_reaction(comment_id)) # noqa E501
if not success: if not success:
git_provider.set_pr(pr_url) git_provider.set_pr(pr_url)
git_provider.publish_comment("### How to use PR-Agent\n" +
bot_help_text(user_id))
elif response.status != 304: elif response.status != 304:
print(f"Failed to fetch notifications. Status code: {response.status}") print(f"Failed to fetch notifications. Status code: {response.status}")

View File

@ -11,11 +11,13 @@ from starlette_context import context
from starlette_context.middleware import RawContextMiddleware 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.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.utils import apply_repo_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider from pr_agent.secret_providers import get_secret_provider
setup_logger(fmt=LoggingFormat.JSON) setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
router = APIRouter() router = APIRouter()
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
@ -29,9 +31,29 @@ def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_c
background_tasks.add_task(PRAgent().handle_request, url, body) background_tasks.add_task(PRAgent().handle_request, url, body)
async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
apply_repo_settings(api_url)
commands = get_settings().get(f"gitlab.{commands_conf}", {})
for command in commands:
try:
split_command = command.split(" ")
command = split_command[0]
args = split_command[1:]
other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, new_command)
except Exception as e:
get_logger().error(f"Failed to perform command {command}: {e}")
@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"} log_context = {"server_type": "gitlab_app"}
get_logger().debug("Received a GitLab webhook")
# Check if the request is authorized
if request.headers.get("X-Gitlab-Token") and secret_provider: if request.headers.get("X-Gitlab-Token") and secret_provider:
request_token = request.headers.get("X-Gitlab-Token") request_token = request.headers.get("X-Gitlab-Token")
secret = secret_provider.get_secret(request_token) secret = secret_provider.get_secret(request_token)
@ -47,27 +69,57 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
elif get_settings().get("GITLAB.SHARED_SECRET"): elif get_settings().get("GITLAB.SHARED_SECRET"):
secret = get_settings().get("GITLAB.SHARED_SECRET") secret = get_settings().get("GITLAB.SHARED_SECRET")
if not request.headers.get("X-Gitlab-Token") == secret: if not request.headers.get("X-Gitlab-Token") == secret:
get_logger().error(f"Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"})) return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
else: else:
get_logger().error(f"Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"})) return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None) gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_token: if not gitlab_token:
get_logger().error(f"No gitlab token found")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"})) 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)) get_logger().info("GitLab data", artifact=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']:
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')
handle_request(background_tasks, url, "/review", log_context) get_logger().info(f"New merge request: {url}")
elif data.get('object_kind') == 'note' and data['event_type'] == 'note': await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context)
elif data.get('object_kind') == 'note' and data['event_type'] == 'note': # comment on MR
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')
get_logger().info(f"A comment has been added to a merge request: {url}")
body = data.get('object_attributes', {}).get('note') body = data.get('object_attributes', {}).get('note')
if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body: # /ask_line
body = handle_ask_line(body, data)
handle_request(background_tasks, url, body, log_context) 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"}))
def handle_ask_line(body, data):
try:
line_range_ = data['object_attributes']['position']['line_range']
# if line_range_['start']['type'] == 'new':
start_line = line_range_['start']['new_line']
end_line = line_range_['end']['new_line']
# else:
# start_line = line_range_['start']['old_line']
# end_line = line_range_['end']['old_line']
question = body.replace('/ask', '').strip()
path = data['object_attributes']['position']['new_path']
side = 'RIGHT' # if line_range_['start']['type'] == 'new' else 'LEFT'
comment_id = data['object_attributes']["discussion_id"]
get_logger().info(f"Handling line comment")
body = f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={comment_id} {question}"
except Exception as e:
get_logger().error(f"Failed to handle ask line comment: {e}")
return body
@router.get("/") @router.get("/")
async def root(): async def root():
return {"status": "ok"} return {"status": "ok"}

View File

@ -1,19 +1,354 @@
commands_text = "> **/review**: Request a review of your Pull Request. \n" \ class HelpMessage:
"> **/describe**: Update the PR title and description based on the contents of the PR. \n" \ @staticmethod
"> **/improve [--extended]**: Suggest code improvements. Extended mode provides a higher quality feedback. \n" \ def get_general_commands_text():
"> **/ask \\<QUESTION\\>**: Ask a question about the PR. \n" \ commands_text = "> - **/review**: Request a review of your Pull Request. \n" \
"> **/update_changelog**: Update the changelog based on the PR's contents. \n" \ "> - **/describe**: Update the PR title and description based on the contents of the PR. \n" \
"> **/add_docs**: Generate docstring for new components introduced in the PR. \n" \ "> - **/improve [--extended]**: Suggest code improvements. Extended mode provides a higher quality feedback. \n" \
"> **/generate_labels**: Generate labels for the PR based on the PR's contents. \n" \ "> - **/ask \\<QUESTION\\>**: Ask a question about the PR. \n" \
"> see the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n\n" \ "> - **/update_changelog**: Update the changelog based on the PR's contents. \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" \ "> - **/add_docs** 💎: Generate docstring for new components introduced in the PR. \n" \
">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \ "> - **/generate_labels** 💎: Generate labels for the PR based on the PR's contents. \n" \
">To list the possible configuration parameters, add a **/config** comment. \n" \ "> - **/analyze** 💎: Automatically analyzes the PR, and presents changes walkthrough for each component. \n\n" \
">See the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n" \
">To list the possible configuration parameters, add a **/config** comment. \n"
return commands_text
def bot_help_text(user: str): @staticmethod
return f"> Tag me in a comment '@{user}' and add one of the following commands: \n" + commands_text def get_general_bot_help_text():
output = f"> To invoke the PR-Agent, add a comment using one of the following commands: \n{HelpMessage.get_general_commands_text()} \n"
return output
@staticmethod
def get_review_usage_guide():
output ="**Overview:**\n"
output +="The `review` tool scans the PR code changes, and generates a PR review. The tool can be triggered [automatically](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) every time a new PR is opened, or can be invoked manually by commenting on any PR.\n"
output +="""\
When commenting, to edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L19) related to the review tool (`pr_reviewer` section), use the following template:
```
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
```
With a [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-app), use the following template:
```
[pr_reviewer]
some_config1=...
some_config2=...
```
"""
output +="\n\n<table>"
# extra instructions
output += "<tr><td><details> <summary><strong> Utilizing extra instructions</strong></summary><hr>\n\n"
output += '''\
The `review` tool can be configured with extra instructions, which can be used to guide the model to a feedback tailored to the needs of your project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify the relevant sub-tool, and the relevant aspects of the PR that you want to emphasize.
Examples for extra instructions:
```
[pr_reviewer] # /review #
extra_instructions="""
In the 'possible issues' section, emphasize the following:
- Does the code logic cover relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
'''
output += "\n\n</details></td></tr>\n\n"
# automation
output += "<tr><td><details> <summary><strong> How to enable\\disable automation</strong></summary><hr>\n\n"
output += """\
- When you first install PR-Agent app, the [default mode](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) for the `review` tool is:
```
pr_commands = ["/review", ...]
```
meaning the `review` tool will run automatically on every PR, with the default configuration.
Edit this field to enable/disable the tool, or to change the used configurations
"""
output += "\n\n</details></td></tr>\n\n"
# # code feedback
# output += "<tr><td><details> <summary><strong> About the 'Code feedback' section</strong></summary><hr>\n\n"
# output+="""\
# The `review` tool provides several type of feedbacks, one of them is code suggestions.
# If you are interested **only** in the code suggestions, it is recommended to use the [`improve`](https://github.com/Codium-ai/pr-agent/blob/main/docs/IMPROVE.md) feature instead, since it dedicated only to code suggestions, and usually gives better results.
# Use the `review` tool if you want to get a more comprehensive feedback, which includes code suggestions as well.
# """
# output += "\n\n</details></td></tr>\n\n"
# auto-labels
output += "<tr><td><details> <summary><strong> Auto-labels</strong></summary><hr>\n\n"
output+="""\
The `review` tool can auto-generate two specific types of labels for a PR:
- a `possible security issue` label, that detects possible [security issues](https://github.com/Codium-ai/pr-agent/blob/tr/user_description/pr_agent/settings/pr_reviewer_prompts.toml#L136) (`enable_review_labels_security` flag)
- a `Review effort [1-5]: x` label, where x is the estimated effort to review the PR (`enable_review_labels_effort` flag)
"""
output += "\n\n</details></td></tr>\n\n"
# extra sub-tools
output += "<tr><td><details> <summary><strong> Extra sub-tools</strong></summary><hr>\n\n"
output += """\
The `review` tool provides a collection of possible feedbacks about a PR.
It is recommended to review the [possible options](https://github.com/Codium-ai/pr-agent/blob/main/docs/REVIEW.md#enabledisable-features), and choose the ones relevant for your use case.
Some of the feature that are disabled by default are quite useful, and should be considered for enabling. For example:
`require_score_review`, `require_soc2_ticket`, and more.
"""
output += "\n\n</details></td></tr>\n\n"
output += "<tr><td><details> <summary><strong> Auto-approve PRs</strong></summary><hr>\n\n"
output += '''\
By invoking:
```
/review auto_approve
```
The tool will automatically approve the PR, and add a comment with the approval.
actions_help_text = "> To invoke the PR-Agent, add a comment using one of the following commands: \n" + \ To ensure safety, the auto-approval feature is disabled by default. To enable auto-approval, you need to actively set in a pre-defined configuration file the following:
commands_text ```
[pr_reviewer]
enable_auto_approval = true
```
(this specific flag cannot be set with a command line argument, only in the configuration file, committed to the repository)
You can also enable auto-approval only if the PR meets certain requirements, such as that the `estimated_review_effort` is equal or below a certain threshold, by adjusting the flag:
```
[pr_reviewer]
maximal_review_effort = 5
```
'''
output += "\n\n</details></td></tr>\n\n"
# general
output += "\n\n<tr><td><details> <summary><strong> More PR-Agent commands</strong></summary><hr> \n\n"
output += HelpMessage.get_general_bot_help_text()
output += "\n\n</details></td></tr>\n\n"
output += "</table>"
output += f"\n\nSee the [review usage](https://github.com/Codium-ai/pr-agent/blob/main/docs/REVIEW.md) page for a comprehensive guide on using this tool.\n\n"
return output
@staticmethod
def get_describe_usage_guide():
output = "**Overview:**\n"
output += "The `describe` tool scans the PR code changes, and generates a description for the PR - title, type, summary, walkthrough and labels. "
output += "The tool can be triggered [automatically](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) every time a new PR is opened, or can be invoked manually by commenting on a PR.\n"
output += """\
When commenting, to edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L46) related to the describe tool (`pr_description` section), use the following template:
```
/describe --pr_description.some_config1=... --pr_description.some_config2=...
```
With a [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-app), use the following template:
```
[pr_description]
some_config1=...
some_config2=...
```
"""
output += "\n\n<table>"
# automation
output += "<tr><td><details> <summary><strong> Enabling\\disabling automation </strong></summary><hr>\n\n"
output += """\
- When you first install the app, the [default mode](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) for the describe tool is:
```
pr_commands = ["/describe --pr_description.add_original_user_description=true"
"--pr_description.keep_original_user_title=true", ...]
```
meaning the `describe` tool will run automatically on every PR, will keep the original title, and will add the original user description above the generated description.
- Markers are an alternative way to control the generated description, to give maximal control to the user. If you set:
```
pr_commands = ["/describe --pr_description.use_description_markers=true", ...]
```
the tool will replace every marker of the form `pr_agent:marker_name` in the PR description with the relevant content, where `marker_name` is one of the following:
- `type`: the PR type.
- `summary`: the PR summary.
- `walkthrough`: the PR walkthrough.
Note that when markers are enabled, if the original PR description does not contain any markers, the tool will not alter the description at all.
"""
output += "\n\n</details></td></tr>\n\n"
# custom labels
output += "<tr><td><details> <summary><strong> Custom labels </strong></summary><hr>\n\n"
output += """\
The default labels of the `describe` tool are quite generic: [`Bug fix`, `Tests`, `Enhancement`, `Documentation`, `Other`].
If you specify [custom labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/DESCRIBE.md#handle-custom-labels-from-the-repos-labels-page-gem) in the repo's labels page or via configuration file, you can get tailored labels for your use cases.
Examples for custom labels:
- `Main topic:performance` - pr_agent:The main topic of this PR is performance
- `New endpoint` - pr_agent:A new endpoint was added in this PR
- `SQL query` - pr_agent:A new SQL query was added in this PR
- `Dockerfile changes` - pr_agent:The PR contains changes in the Dockerfile
- ...
The list above is eclectic, and aims to give an idea of different possibilities. Define custom labels that are relevant for your repo and use cases.
Note that Labels are not mutually exclusive, so you can add multiple label categories.
Make sure to provide proper title, and a detailed and well-phrased description for each label, so the tool will know when to suggest it.
"""
output += "\n\n</details></td></tr>\n\n"
# Inline File Walkthrough
output += "<tr><td><details> <summary><strong> Inline File Walkthrough 💎</strong></summary><hr>\n\n"
output += """\
For enhanced user experience, the `describe` tool can add file summaries directly to the "Files changed" tab in the PR page.
This will enable you to quickly understand the changes in each file, while reviewing the code changes (diffs).
To enable inline file summary, set `pr_description.inline_file_summary` in the configuration file, possible values are:
- `'table'`: File changes walkthrough table will be displayed on the top of the "Files changed" tab, in addition to the "Conversation" tab.
- `true`: A collapsable file comment with changes title and a changes summary for each file in the PR.
- `false` (default): File changes walkthrough will be added only to the "Conversation" tab.
"""
# extra instructions
output += "<tr><td><details> <summary><strong> Utilizing extra instructions</strong></summary><hr>\n\n"
output += '''\
The `describe` tool can be configured with extra instructions, to guide the model to a feedback tailored to the needs of your project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Notice that the general structure of the description is fixed, and cannot be changed. Extra instructions can change the content or style of each sub-section of the PR description.
Examples for extra instructions:
```
[pr_description]
extra_instructions="""
- The PR title should be in the format: '<PR type>: <title>'
- The title should be short and concise (up to 10 words)
- ...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
'''
output += "\n\n</details></td></tr>\n\n"
# general
output += "\n\n<tr><td><details> <summary><strong> More PR-Agent commands</strong></summary><hr> \n\n"
output += HelpMessage.get_general_bot_help_text()
output += "\n\n</details></td></tr>\n\n"
output += "</table>"
output += f"\n\nSee the [describe usage](https://github.com/Codium-ai/pr-agent/blob/main/docs/DESCRIBE.md) page for a comprehensive guide on using this tool.\n\n"
return output
@staticmethod
def get_ask_usage_guide():
output = "**Overview:**\n"
output += """\
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 "..."
```
Note that the tool does not have "memory" of previous questions, and answers each question independently.
"""
output += "\n\n<table>"
# general
output += "\n\n<tr><td><details> <summary><strong> More PR-Agent commands</strong></summary><hr> \n\n"
output += HelpMessage.get_general_bot_help_text()
output += "\n\n</details></td></tr>\n\n"
output += "</table>"
output += f"\n\nSee the [ask usage](https://github.com/Codium-ai/pr-agent/blob/main/docs/ASK.md) page for a comprehensive guide on using this tool.\n\n"
return output
@staticmethod
def get_improve_usage_guide():
output = "**Overview:**\n"
output += "The `improve` tool scans the PR code changes, and automatically generates suggestions for improving the PR code. "
output += "The tool can be triggered [automatically](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) every time a new PR is opened, or can be invoked manually by commenting on a PR.\n"
output += """\
When commenting, to edit [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L69) related to the improve tool (`pr_code_suggestions` section), use the following template:
```
/improve --pr_code_suggestions.some_config1=... --pr_code_suggestions.some_config2=...
```
With a [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-app), use the following template:
```
[pr_code_suggestions]
some_config1=...
some_config2=...
```
"""
output += "\n\n<table>"
# automation
output += "<tr><td><details> <summary><strong> Enabling\\disabling automation </strong></summary><hr>\n\n"
output += """\
When you first install the app, the [default mode](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) for the improve tool is:
```
pr_commands = ["/improve --pr_code_suggestions.summarize=true", ...]
```
meaning the `improve` tool will run automatically on every PR, with summarization enabled. Delete this line to disable the tool from running automatically.
"""
output += "\n\n</details></td></tr>\n\n"
# extra instructions
output += "<tr><td><details> <summary><strong> Utilizing extra instructions</strong></summary><hr>\n\n"
output += '''\
Extra instructions are very important for the `improve` tool, since they enable to guide the model to suggestions that are more relevant to the specific needs of the project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify relevant aspects that you want the model to focus on.
Examples for extra instructions:
```
[pr_code_suggestions] # /improve #
extra_instructions="""
Emphasize the following aspects:
- Does the code logic cover relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
'''
output += "\n\n</details></td></tr>\n\n"
# suggestions quality
output += "\n\n<tr><td><details> <summary><strong> A note on code suggestions quality</strong></summary><hr> \n\n"
output += """\
- 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. 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, or use the [custom suggestions :gem:](https://github.com/Codium-ai/pr-agent/blob/main/docs/CUSTOM_SUGGESTIONS.md) tool
- With large PRs, best quality will be obtained by using 'improve --extended' mode.
"""
output += "\n\n</details></td></tr>\n\n"\
# general
output += "\n\n<tr><td><details> <summary><strong> More PR-Agent commands</strong></summary><hr> \n\n"
output += HelpMessage.get_general_bot_help_text()
output += "\n\n</details></td></tr>\n\n"
output += "</table>"
output += f"\n\nSee the [improve usage](https://github.com/Codium-ai/pr-agent/blob/main/docs/IMPROVE.md) page for a more comprehensive guide on using this tool.\n\n"
return output

View File

@ -65,9 +65,25 @@ personal_access_token = ""
# For Bitbucket personal/repository bearer token # For Bitbucket personal/repository bearer token
bearer_token = "" bearer_token = ""
[bitbucket_server]
# For Bitbucket Server bearer token
auth_token = ""
webhook_secret = ""
# For Bitbucket app # For Bitbucket app
app_key = "" app_key = ""
base_url = "" base_url = ""
[litellm] [litellm]
LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token
[azure_devops]
# For Azure devops personal access token
org = ""
pat = ""
[azure_devops_server]
# For Azure devops Server basic auth - configured in the webhook creation
# Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook
# webhook_username = "<basic auth user>"
# webhook_password = "<basic auth password>"

View File

@ -1,56 +1,70 @@
[config] [config]
model="gpt-4" # "gpt-4-1106-preview" model="gpt-4" # "gpt-4-0125-preview"
model_turbo="gpt-4-0125-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
publish_output_progress=true publish_output_progress=true
verbosity_level=0 # 0,1,2 verbosity_level=0 # 0,1,2
use_extra_bad_extensions=false use_extra_bad_extensions=false
use_wiki_settings_file=true
use_repo_settings_file=true use_repo_settings_file=true
ai_timeout=180 use_global_settings_file=true
ai_timeout=120 # 2minutes
max_description_tokens = 500 max_description_tokens = 500
max_commits_tokens = 500 max_commits_tokens = 500
max_model_tokens = 32000 # Limits the maximum number of tokens that can be used by any model, regardless of the model's default capabilities. 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 patch_extra_lines = 1
secret_provider="google_cloud_storage" secret_provider="google_cloud_storage"
cli_mode=false cli_mode=false
ai_disclaimer_title="" # Pro feature, title for a collapsible disclaimer to AI outputs
ai_disclaimer="" # Pro feature, full text for the AI disclaimer
[pr_reviewer] # /review # [pr_reviewer] # /review #
# enable/disable features # 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_estimate_effort_to_review=true require_estimate_effort_to_review=true
# soc2
require_soc2_ticket=false
soc2_ticket_prompt="Does the PR description include a link to ticket in a project management system (e.g., Jira, Asana, Trello, etc.) ?"
# general options # 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 persistent_comment=true
extra_instructions = "" extra_instructions = ""
final_update_message = true
# review labels # review labels
enable_review_labels_security=true enable_review_labels_security=true
enable_review_labels_effort=false enable_review_labels_effort=true
# specific configurations for incremental review (/review -i) # specific configurations for incremental review (/review -i)
require_all_thresholds_for_incremental_review=false require_all_thresholds_for_incremental_review=false
minimal_commits_for_incremental_review=0 minimal_commits_for_incremental_review=0
minimal_minutes_for_incremental_review=0 minimal_minutes_for_incremental_review=0
enable_help_text=true # Determines whether to include help text in the PR review. Enabled by default.
# auto approval
enable_auto_approval=false
maximal_review_effort=5
[pr_description] # /describe # [pr_description] # /describe #
publish_labels=true publish_labels=true
publish_description_as_comment=false publish_description_as_comment=false
add_original_user_description=false add_original_user_description=true
keep_original_user_title=false keep_original_user_title=true
use_bullet_points=true use_bullet_points=true
extra_instructions = "" extra_instructions = ""
enable_pr_type=true enable_pr_type=true
enable_file_walkthrough=false
enable_semantic_files_types=true
final_update_message = true final_update_message = true
enable_help_text=false
enable_help_comment=true
## changes walkthrough section
enable_semantic_files_types=true
collapsible_file_list='adaptive' # true, false, 'adaptive'
inline_file_summary=false # false, true, 'table'
# markers # markers
use_description_markers=false use_description_markers=false
include_generated_by_header=true include_generated_by_header=true
@ -58,17 +72,23 @@ include_generated_by_header=true
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other'] #custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
[pr_questions] # /ask # [pr_questions] # /ask #
enable_help_text=true
[pr_code_suggestions] # /improve # [pr_code_suggestions] # /improve #
max_context_tokens=8000
num_code_suggestions=4 num_code_suggestions=4
summarize = false summarize = true
extra_instructions = "" extra_instructions = ""
rank_suggestions = false rank_suggestions = false
enable_help_text=true
# params for '/improve --extended' mode # params for '/improve --extended' mode
num_code_suggestions_per_chunk=8 auto_extended_mode=true
rank_extended_suggestions = true num_code_suggestions_per_chunk=5
max_number_of_calls = 5 max_number_of_calls = 3
final_clip_factor = 0.9 parallel_calls = true
rank_extended_suggestions = false
final_clip_factor = 0.8
[pr_add_docs] # /add_docs # [pr_add_docs] # /add_docs #
extra_instructions = "" extra_instructions = ""
@ -78,6 +98,26 @@ docs_style = "Sphinx Style" # "Google Style with Args, Returns, Attributes...etc
push_changelog_changes=false push_changelog_changes=false
extra_instructions = "" extra_instructions = ""
[pr_analyze] # /analyze #
[pr_test] # /test #
extra_instructions = ""
testing_framework = "" # specify the testing framework you want to use
num_tests=3 # number of tests to generate. max 5.
avoid_mocks=true # if true, the generated tests will prefer to use real objects instead of mocks
file = "" # in case there are several components with the same name, you can specify the relevant file
class_name = "" # in case there are several methods with the same name in the same file, you can specify the relevant class name
enable_help_text=true
[checks] # /checks (pro feature) #
enable_auto_checks_feedback=true
excluded_checks_list=["lint"] # list of checks to exclude, for example: ["check1", "check2"]
persistent_comment=true
enable_help_text=true
[pr_help] # /help #
[pr_config] # /config # [pr_config] # /config #
[github] [github]
@ -85,25 +125,23 @@ extra_instructions = ""
deployment_type = "user" deployment_type = "user"
ratelimit_retries = 5 ratelimit_retries = 5
base_url = "https://api.github.com" base_url = "https://api.github.com"
publish_inline_comments_fallback_with_verification = true
try_fix_invalid_inline_comments = true
[github_action] [github_action_config]
# auto_review = true # set as env var in .github/workflows/pr-agent.yaml # auto_review = true # set as env var in .github/workflows/pr-agent.yaml
# auto_describe = 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 # 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
bot_user = "github-actions[bot]"
override_deployment_type = true override_deployment_type = true
# in some deployments it's possible to get duplicate requests if the handling is long,
# these settings are used to avoid handling duplicate requests.
duplicate_requests_cache = false
duplicate_requests_cache_ttl = 60 # in seconds
# settings for "pull_request" event # settings for "pull_request" event
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested'] handle_pr_actions = ['opened', 'reopened', 'ready_for_review']
pr_commands = [ 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", "/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.summarize=true",
] ]
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits # settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
handle_push_trigger = false handle_push_trigger = false
@ -114,34 +152,24 @@ push_trigger_pending_tasks_backlog = true
push_trigger_pending_tasks_ttl = 300 push_trigger_pending_tasks_ttl = 300
push_commands = [ push_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 -i \ "/review --pr_reviewer.num_code_suggestions=0",
--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='' \
"""
] ]
ignore_pr_title = []
[gitlab] [gitlab]
# URL to the gitlab service url = "https://gitlab.com" # URL to the gitlab service
url = "https://gitlab.com" pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.summarize=true",
]
# Polling (either project id or namespace/project_name) syntax can be used [bitbucket_app]
projects_to_monitor = ['org_name/repo_name'] pr_commands = [
"/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.summarize=false",
]
# Polling trigger
magic_word = "AutoReview"
# Polling interval
polling_interval_seconds = 30
[local] [local]
# LocalGitProvider settings - uncomment to use paths other than default # LocalGitProvider settings - uncomment to use paths other than default
@ -158,6 +186,11 @@ polling_interval_seconds = 30
# token to authenticate in the patch server # token to authenticate in the patch server
# patch_server_token = "" # patch_server_token = ""
[bitbucket_server]
# URL to the BitBucket Server instance
# url = "https://git.bitbucket.com"
url = ""
[litellm] [litellm]
#use_client = false #use_client = false
@ -165,8 +198,12 @@ polling_interval_seconds = 30
skip_comments = false skip_comments = false
force_update_dataset = false force_update_dataset = false
max_issues_to_scan = 500 max_issues_to_scan = 500
vectordb = "pinecone"
[pinecone] [pinecone]
# fill and place in .secrets.toml # fill and place in .secrets.toml
#api_key = ... #api_key = ...
# environment = "gcp-starter" # environment = "gcp-starter"
[lancedb]
uri = "./lancedb"

View File

@ -5,7 +5,7 @@ Your task is to generate {{ docs_for_language }} for code components in the PR D
Example for the PR Diff format: Example for the PR Diff format:
====== ======
## src/file1.py ## file: 'src/file1.py'
@@ -12,3 +12,4 @@ def func1(): @@ -12,3 +12,4 @@ def func1():
__new hunk__ __new hunk__
@ -18,7 +18,6 @@ __old hunk__
-code line that was removed in the PR -code line that was removed in the PR
code line2 that remained unchanged in the PR code line2 that remained unchanged in the PR
@@ ... @@ def func2(): @@ ... @@ def func2():
__new hunk__ __new hunk__
... ...
@ -26,7 +25,7 @@ __old hunk__
... ...
## src/file2.py ## file: 'src/file2.py'
... ...
====== ======
@ -35,7 +34,7 @@ Specific instructions:
- Try to identify edited/added code components (classes/functions/methods...) that are undocumented, and generate {{ docs_for_language }} for each one. - Try to identify edited/added code components (classes/functions/methods...) that are undocumented, and generate {{ docs_for_language }} for each one.
- If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them. - If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them.
- Ignore code components that don't appear fully in the '__new hunk__' section. For example, you must see the component header and body. - Ignore code components that don't appear fully in the '__new hunk__' section. For example, you must see the component header and body.
- Make sure the {{ docs_for_language }} starts and ends with standart {{ language }} {{ docs_for_language }} signs. - Make sure the {{ docs_for_language }} starts and ends with standard {{ language }} {{ docs_for_language }} signs.
- The {{ docs_for_language }} should be in standard format. - The {{ docs_for_language }} should be in standard format.
- Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added. - Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added.
@ -69,6 +68,7 @@ Code Documentation:
- after - after
description: |- description: |-
The {{ docs_for_language }} placement relative to the relevant line (code component). The {{ docs_for_language }} placement relative to the relevant line (code component).
For example, in Python the docs are placed after the function signature, but in Java they are placed before.
documentation: documentation:
type: string type: string
description: |- description: |-

View File

@ -4,19 +4,17 @@ Your task is to provide meaningful and actionable code suggestions, to improve t
Example for the PR Diff format: Example for the PR Diff format:
====== ======
## src/file1.py ## file: 'src/file1.py'
@@ -12,3 +12,4 @@ def func1(): @@ ... @@ def func1():
__new hunk__ __new hunk__
12 code line1 that remained unchanged in the PR 12 code line1 that remained unchanged in the PR
14 +new code line1 added in the PR 13 +new hunk code line2 added in the PR
15 +new code line2 added in the PR 14 code line3 that remained unchanged in the PR
16 code line2 that remained unchanged in the PR
__old hunk__ __old hunk__
code line1 that remained unchanged in the PR code line1 that remained unchanged in the PR
-code line that was removed in the PR -old hunk code line2 that was removed in the PR
code line2 that remained unchanged in the PR code line3 that remained unchanged in the PR
@@ ... @@ def func2(): @@ ... @@ def func2():
__new hunk__ __new hunk__
@ -25,24 +23,24 @@ __old hunk__
... ...
## src/file2.py ## file: 'src/file2.py'
... ...
====== ======
Specific instructions: Specific instructions:
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. - Provide up to {{ num_code_suggestions }} code suggestions. The suggestions should be diverse and insightful.
- 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. - The suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+').
- Don't suggest to add docstring, type hints, or comments. - Prioritize suggestions that address major problems, issues and bugs in the PR code. As a second priority, suggestions should focus on enhancement, best practice, performance, maintainability, and other aspects.
- Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+'). - Don't suggest to add docstring, type hints, or comments, or to remove unused imports.
- 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. - Suggestions should not repeat code already present in the '__new hunk__' sections.
- 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 suggestion. - Provide the exact line numbers range (inclusive) for each suggestion.
- Assume there is additional relevant code, that is not included in the diff. - When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
====== ======
{{ extra_instructions }} {{ extra_instructions }}
@ -50,85 +48,67 @@ Extra instructions from the user:
{%- endif %} {%- endif %}
You must use the following YAML schema to format your answer: The output must be a YAML object equivalent to type $PRCodeSuggestions, according to the following Pydantic definitions:
```yaml =====
Code suggestions: class CodeSuggestion(BaseModel):
type: array relevant_file: str = Field(description="the relevant file full path")
minItems: 1 language: str = Field(description="the code language of the relevant file")
maxItems: {{ num_code_suggestions }} suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving the new code introduced in the PR")
uniqueItems: true {%- if summarize_mode %}
items: existing_code: str = Field(description="a short code snippet from a '__new hunk__' section to illustrate the relevant existing code. Don't show the line numbers.")
relevant file: improved_code: str = Field(description="a short code snippet to illustrate the improved code, after applying the suggestion.")
type: string one_sentence_summary:str = Field(description="a short summary of the suggestion action, in a single sentence. Focus on the 'what'. Be general, and avoid method or variable names.")
description: the relevant file full path {%- else %}
suggestion content: existing_code: str = Field(description="a code snippet, demonstrating the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers")
type: string improved_code: str = Field(description="a new code snippet, that can be used to replace the relevant lines in '__new hunk__' code. Replacement suggestions should be complete, correctly formatted and indented, and without line numbers")
description: |- {%- endif %}
a concrete suggestion for meaningfully improving the new PR code. relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above")
existing code: relevant_lines_end: int = Field(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")
type: string label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'bug', 'performance', 'enhancement', 'possible issue', 'best practice', 'maintainability', etc. Other labels are also allowed")
description: |-
a code snippet showing the relevant code lines from a '__new hunk__' section. class PRCodeSuggestions(BaseModel):
It must be contiguous, correctly formatted and indented, and without line numbers. code_suggestions: List[CodeSuggestion]
relevant lines start: =====
type: integer
description: |-
The relevant line number from a '__new hunk__' section where the suggestion starts (inclusive).
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:
type: string
description: |-
a new code snippet that can be used to replace the relevant lines in '__new hunk__' code.
Replacement suggestions should be complete, correctly formatted and indented, and without line numbers.
```
Example output: Example output:
```yaml ```yaml
Code suggestions: code_suggestions:
- relevant file: |- - relevant_file: |
src/file1.py src/file1.py
suggestion content: |- language: |
Add a docstring to func1() python
existing code: |- suggestion_content: |
def func1(): ...
relevant lines start: |- {%- if summarize_mode %}
12 existing_code: |
relevant lines end: |- ...
12 improved_code: |
improved code: |- ...
one_sentence_summary: |
...
relevant_lines_start: 12
relevant_lines_end: 13
{%- else %}
existing_code: |
...
relevant_lines_start: 12
relevant_lines_end: 13
improved_code: |
...
{%- endif %}
label: |
... ...
...
``` ```
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-'). 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: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %}
Main PR language: '{{ language }}'
{%- endif %}
The PR Diff: The PR Diff:
====== ======

View File

@ -30,7 +30,7 @@ class Label(str, Enum):
{%- endif %} {%- endif %}
class Labels(BaseModel): class Labels(BaseModel):
labels: List[Label] = Field(min_items=0, description="custom labels that describe the PR. Return the label value, not the name.") labels: List[Label] = Field(min_items=0, description="choose the relevant custom labels that describe the PR content, and return their keys. Use the value field of the Label object to better understand the label meaning.")
====== ======

View File

@ -1,16 +1,21 @@
[pr_description_prompt] [pr_description_prompt]
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR). system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
Your task is to provide a full description for the PR content - title, type, description, and main files walkthrough. {%- if enable_custom_labels %}
Your task is to provide a full description for the PR content - files walkthrough, title, type, description and labels.
{%- else %}
Your task is to provide a full description for the PR content - files walkthrough, title, type, and description.
{%- endif %}
- Focus on the new PR code (lines starting with '+'). - Focus on the new PR code (lines starting with '+').
- 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. - 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.
- The generated title and description should prioritize the most significant changes. - The generated title and description should prioritize the most significant changes.
- If needed, each YAML output should be in block scalar indicator ('|-') - If needed, each YAML output should be in block scalar indicator ('|-')
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
===== =====
{{ extra_instructions }} {{extra_instructions}}
===== =====
{% endif %} {% endif %}
@ -30,31 +35,25 @@ class PRType(str, Enum):
{%- endif %} {%- endif %}
{%- if enable_file_walkthrough %}
class FileWalkthrough(BaseModel):
filename: str = Field(description="the relevant file full path")
changes_in_file: str = Field(description="minimal and concise summary of the changes in the relevant file")
{%- endif %}
{%- if enable_semantic_files_types %} {%- if enable_semantic_files_types %}
Class FileDescription(BaseModel): Class FileDescription(BaseModel):
filename: str = Field(description="the relevant file full path") filename: str = Field(description="the relevant file full path")
changes_summary: str = Field(description="minimal and concise summary of the changes in the relevant file") language: str = Field(description="the relevant file language")
changes_summary: str = Field(description="concise summary of the changes in the relevant file, in bullet points (1-4 bullet points).")
changes_title: str = Field(description="an informative title for the changes in the files, describing its main theme (5-10 words).")
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', ...") 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', ...")
{%- endif %} {%- endif %}
Class PRDescription(BaseModel): Class PRDescription(BaseModel):
title: str = Field(description="an informative title for the PR, describing its main theme") type: List[PRType] = Field(description="one or more types that describe the PR content. Return the label member value (e.g. 'Bug fix', not 'bug_fix')")
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 %} {%- if enable_semantic_files_types %}
pr_files[List[FileDescription]] = Field(max_items=15") pr_files[List[FileDescription]] = Field(max_items=15, description="a list of the files in the PR, and their changes summary.")
{%- endif %}
description: str = Field(description="an informative and concise description of the PR. Use bullet points. Display first the most significant changes.")
title: str = Field(description="an informative title for the PR, describing its main theme")
{%- if enable_custom_labels %}
labels: List[Label] = Field(min_items=0, description="choose the relevant custom labels that describe the PR content, and return their keys. Use the value field of the Label object to better understand the label meaning.")
{%- endif %} {%- endif %}
===== =====
@ -62,36 +61,37 @@ Class PRDescription(BaseModel):
Example output: Example output:
```yaml ```yaml
title: |-
...
type: type:
- ... - ...
- ... - ...
{%- if enable_custom_labels %}
labels:
- ...
- ...
{%- endif %}
description: |-
...
{%- if enable_file_walkthrough %}
main_files_walkthrough:
- ...
- ...
{%- endif %}
{%- if enable_semantic_files_types %} {%- if enable_semantic_files_types %}
pr_files: pr_files:
- filename: | - filename: |
... ...
language: |
...
changes_summary: | changes_summary: |
... ...
changes_title: |
...
label: | label: |
... ...
... ...
{%- endif %} {%- endif %}
description: |-
...
title: |-
...
{%- if enable_custom_labels %}
labels:
- |
...
- |
...
{%- endif %}
``` ```
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|-') 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:
@ -107,10 +107,7 @@ Previous description:
{%- endif %} {%- endif %}
Branch: '{{branch}}' Branch: '{{branch}}'
{%- if language %}
Main PR language: '{{ language }}'
{%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:

View File

@ -0,0 +1,53 @@
[pr_line_questions_prompt]
system="""You are PR-Reviewer, a language model designed to answer questions about a Git Pull Request (PR).
Your goal is to answer questions\\tasks about specific lines of code in the PR, and provide feedback.
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 any unrelated content.
Additional guidelines:
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
- If relevant, use bullet points.
- Be short and to the point.
Example Hunk Structure:
======
## file: 'src/file1.py'
@@ -12,5 +12,5 @@ def func1():
code line 1 that remained unchanged in the PR
code line 2 that remained unchanged in the PR
-code line that was removed in the PR
+code line added in the PR
code line 3 that remained unchanged in the PR
======
"""
user="""PR Info:
Title: '{{title}}'
Branch: '{{branch}}'
Here is a context hunk from the PR diff:
======
{{ full_hunk|trim }}
======
Now focus on the selected lines from the hunk:
======
{{ selected_lines|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
A question about the selected lines:
======
{{ question|trim }}
======
Response to the question:
"""

View File

@ -1,7 +1,7 @@
[pr_questions_prompt] [pr_questions_prompt]
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR). system="""You are PR-Reviewer, a language model designed to answer questions about a Git Pull Request (PR).
Your goal is to answer questions\\tasks about the new PR code (lines starting with '+'), and provide feedback. Your goal is to answer questions\\tasks about the new code introduced in the PR (lines starting with '+' in the 'PR Git Diff' section), 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 any unrelated content. Don't avoid answering the questions. You must answer the questions, as best as you can, without adding any unrelated content.
""" """

View File

@ -1,11 +1,15 @@
[pr_review_prompt] [pr_review_prompt]
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR). system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
{%- if num_code_suggestions > 0 %}
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.
{%- else %}
Your task is to provide constructive and concise feedback for the PR.
{%- endif %}
The review should focus on new code added in the PR diff (lines starting with '+') The review should focus on new code added in the PR diff (lines starting with '+')
Example PR Diff: Example PR Diff:
====== ======
## src/file1.py ## file: 'src/file1.py'
@@ -12,5 +12,5 @@ def func1(): @@ -12,5 +12,5 @@ def func1():
code line 1 that remained unchanged in the PR code line 1 that remained unchanged in the PR
@ -14,12 +18,11 @@ code line 2 that remained unchanged in the PR
+code line added in the PR +code line added in the PR
code line 3 that remained unchanged in the PR code line 3 that remained unchanged in the PR
@@ ... @@ def func2(): @@ ... @@ def func2():
... ...
## src/file2.py ## file: 'src/file2.py'
... ...
====== ======
@ -28,10 +31,11 @@ code line 3 that remained unchanged in the PR
Code suggestions guidelines: Code suggestions guidelines:
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. - Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices. - Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements like performance, vulnerability, modularity, and best practices.
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code. - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
- Don't suggest to add docstring, type hints, or comments. - Don't suggest to add docstring, type hints, or comments.
- Suggestions should focus on the new code added in the PR diff (lines starting with '+') - Suggestions should focus on the new code added in the PR diff (lines starting with '+')
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- endif %} {%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
@ -43,141 +47,81 @@ Extra instructions from the user:
{% endif %} {% endif %}
You must use the following YAML schema to format your answer: The output must be a YAML object equivalent to type $PRReview, according to the following Pydantic definitions:
```yaml =====
PR Analysis: class Review(BaseModel):
Main theme: {%- if require_estimate_effort_to_review %}
type: string estimated_effort_to_review_[1-5]: str = Field(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 in a short and concise manner.")
description: a short explanation of the PR {%- endif %}
PR summary:
type: string
description: summary of the PR in 2-3 sentences.
Type of PR:
type: string
enum:
- Bug fix
- Tests
- Enhancement
- Documentation
- Other
{%- if require_score %} {%- if require_score %}
Score: score: str = Field(description="Rate this PR on a scale of 0-100 (inclusive), where 0 means the worst possible PR code, and 100 means PR code of the highest quality, without any bugs or performance issues, that is ready to be merged immediately and run in production at scale.")
type: int
description: |-
Rate this PR on a scale of 0-100 (inclusive), where 0 means the worst
possible PR code, and 100 means PR code of the highest quality, without
any bugs or performance issues, that is ready to be merged immediately and
run in production at scale.
{%- endif %} {%- endif %}
{%- if require_tests %} {%- if require_tests %}
Relevant tests added: relevant_tests: str = Field(description="yes\\no question: does this PR have relevant tests added or updated ?")
type: string
description: yes\\no question: does this PR have relevant tests ?
{%- endif %} {%- endif %}
{%- if question_str %} {%- if question_str %}
Insights from user's answer: insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
type: string
description: |-
shortly summarize the insights you gained from the user's answers to the questions
{%- endif %} {%- endif %}
{%- if require_focused %} {%- if require_focused %}
Focused PR: focused_pr: str = Field(description="Is this a focused PR, in the sense that all the PR code diff changes are united under a single focused theme ? If the theme is too broad, or the PR code diff changes are too scattered, then the PR is not focused. Explain your answer shortly.")
type: string
description: |-
Is this a focused PR, in the sense that all the PR code diff changes are
united under a single focused theme ? If the theme is too broad, or the PR
code diff changes are too scattered, then the PR is not focused. Explain
your answer shortly.
{%- endif %} {%- endif %}
{%- if require_estimate_effort_to_review %} possible_issues: str = Field(description="Does this PR code introduce clear issues, bugs, or major performance concerns? If there are no apparent issues, respond with 'No'. If there are any issues, describe them briefly. Use bullet points if more than one issue. Be specific, and provide examples if possible. Start each bullet point with a short specific header, such as: "- Possible Bug: ...", etc.")
Estimated effort to review [1-5]: security_concerns: str = Field(description="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. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...' etc. Explain your answer. Be specific and give examples if possible")
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:
General suggestions:
type: string
description: |-
General suggestions and feedback for the contributors and maintainers of this PR.
May include important suggestions for the overall structure,
primary purpose, best practices, critical bugs, and other aspects of the PR.
Don't address PR title and description, or lack of tests. Explain your suggestions.
{%- if num_code_suggestions > 0 %} {%- if num_code_suggestions > 0 %}
Code feedback:
type: array class CodeSuggestion(BaseModel):
maxItems: {{ num_code_suggestions }} relevant_file: str = Field(description="the relevant file full path")
uniqueItems: true language: str = Field(description="the language of the relevant file")
items: suggestion: str = Field(description="a concrete suggestion for meaningfully improving the new PR code. Also describe how, specifically, the suggestion can be applied to new PR code. Add tags with importance measure that matches each suggestion ('important' or 'medium'). Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.")
relevant file: relevant_line: str = Field(description="a single code line taken from the relevant file, to which the suggestion applies. The code line should start with a '+'. Make sure to output the line exactly as it appears in the relevant file")
type: string
description: the relevant file full path
suggestion:
type: string
description: |-
a concrete suggestion for meaningfully improving the new PR code.
Also describe how, specifically, the suggestion can be applied to new PR code.
Add tags with importance measure that matches each suggestion ('important' or 'medium').
Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.
relevant line:
type: string
description: |-
a single code line taken from the relevant file, to which the suggestion applies.
The code line should start with a '+'.
Make sure to output the line exactly as it appears in the relevant file
{%- endif %} {%- endif %}
{%- if require_security %} {%- if num_code_suggestions > 0 %}
Security concerns:
type: string class PRReview(BaseModel):
description: >- review: Review
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. code_feedback: List[CodeSuggestion]
Answer 'Yes, because ...' if there are security concerns or issues. Explain your answer shortly. {%- else %}
class PRReview(BaseModel):
review: Review
{%- endif %} {%- endif %}
``` =====
Example output: Example output:
```yaml ```yaml
PR Analysis: review:
Main theme: |-
xxx
PR summary: |-
xxx
Type of PR: |-
...
{%- if require_score %}
Score: 89
{%- endif %}
Relevant tests added: |-
No
{%- if require_focused %}
Focused PR: no, because ...
{%- endif %}
{%- if require_estimate_effort_to_review %} {%- if require_estimate_effort_to_review %}
Estimated effort to review [1-5]: |- estimated_effort_to_review_[1-5]: |
3, because ... 3, because ...
{%- endif %} {%- endif %}
PR Feedback: {%- if require_score %}
General PR suggestions: |- score: 89
...
{%- if num_code_suggestions > 0 %}
Code feedback:
- relevant file: |-
directory/xxx.py
suggestion: |-
xxx [important]
relevant line: |-
xxx
...
{%- endif %} {%- endif %}
{%- if require_security %} relevant_tests: |
Security concerns: No No
{%- if require_focused %}
focused_pr: |
no, because ...
{%- endif %}
possible_issues: |
No
security_concerns: |
No
{%- if num_code_suggestions > 0 %}
code_feedback
- relevant_file: |
directory/xxx.py
language: |
python
suggestion: |
xxx [important]
relevant_line: |
xxx
{%- endif %} {%- endif %}
``` ```
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-'). Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
""" """
user="""PR Info: user="""PR Info:
@ -194,19 +138,8 @@ Description:
====== ======
{%- endif %} {%- endif %}
{%- if language %}
Main PR language: '{{ language }}'
{%- endif %}
{%- if commit_messages_str %}
Commit messages:
======
{{commit_messages_str}}
======
{%- 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.

View File

@ -1,10 +1,12 @@
import copy import copy
import textwrap import textwrap
from functools import partial
from typing import Dict from typing import Dict
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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
@ -15,14 +17,15 @@ from pr_agent.log import get_logger
class PRAddDocs: class PRAddDocs:
def __init__(self, pr_url: str, cli_mode=False, args: list = None): def __init__(self, pr_url: str, cli_mode=False, args: list = None,
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language( self.main_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.ai_handler = AiHandler() self.ai_handler = ai_handler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.cli_mode = cli_mode self.cli_mode = cli_mode

View File

@ -1,29 +1,43 @@
import asyncio
import copy import copy
import textwrap import textwrap
from functools import partial
from typing import Dict, List from typing import Dict, List
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models 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, replace_code_tags, ModelType
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 from pr_agent.log import get_logger
from pr_agent.servers.help import HelpMessage
from pr_agent.tools.pr_description import insert_br_after_x_chars
import difflib
class PRCodeSuggestions: class PRCodeSuggestions:
def __init__(self, pr_url: str, cli_mode=False, args: list = None): def __init__(self, pr_url: str, cli_mode=False, args: list = None,
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language( self.main_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()
) )
# limit context specifically for the improve command, which has hard input to parse:
if get_settings().pr_code_suggestions.max_context_tokens:
MAX_CONTEXT_TOKENS_IMPROVE = get_settings().pr_code_suggestions.max_context_tokens
if get_settings().config.max_model_tokens > MAX_CONTEXT_TOKENS_IMPROVE:
get_logger().info(f"Setting max_model_tokens to {MAX_CONTEXT_TOKENS_IMPROVE} for PR improve")
get_settings().config.max_model_tokens = MAX_CONTEXT_TOKENS_IMPROVE
# extended mode # extended mode
try: try:
self.is_extended = any(["extended" in arg for arg in args]) self.is_extended = self._get_is_extended(args or [])
except: except:
self.is_extended = False self.is_extended = False
if self.is_extended: if self.is_extended:
@ -31,7 +45,7 @@ class PRCodeSuggestions:
else: else:
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions
self.ai_handler = AiHandler() self.ai_handler = ai_handler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.cli_mode = cli_mode self.cli_mode = cli_mode
@ -42,6 +56,7 @@ class PRCodeSuggestions:
"language": self.main_language, "language": self.main_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
"num_code_suggestions": num_code_suggestions, "num_code_suggestions": num_code_suggestions,
"summarize_mode": get_settings().pr_code_suggestions.summarize,
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions, "extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
} }
@ -50,98 +65,175 @@ class PRCodeSuggestions:
get_settings().pr_code_suggestions_prompt.system, get_settings().pr_code_suggestions_prompt.system,
get_settings().pr_code_suggestions_prompt.user) get_settings().pr_code_suggestions_prompt.user)
self.progress = f"## Generating PR code suggestions\n\n"
self.progress += f"""\nWork in progress ...<br>\n<img src="https://codium.ai/images/pr_agent/dual_ball_loading-crop.gif" width=48>"""
self.progress_response = None
async def run(self): async def run(self):
try: try:
get_logger().info('Generating code suggestions for PR...') get_logger().info('Generating code suggestions for PR...')
relevant_configs = {'pr_code_suggestions': dict(get_settings().pr_code_suggestions),
'config': dict(get_settings().config)}
get_logger().debug("Relevant configs", artifacts=relevant_configs)
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing suggestions...", is_temporary=True) if self.git_provider.is_supported("gfm_markdown"):
self.progress_response = self.git_provider.publish_comment(self.progress)
else:
self.git_provider.publish_comment("Preparing suggestions...", is_temporary=True)
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, ModelType.TURBO)
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, ModelType.TURBO)
if (not data) or (not 'Code suggestions' in data):
get_logger().info('No code suggestions found for PR.') if (not data) or (not 'code_suggestions' in data) or (not data['code_suggestions']):
get_logger().error('No code suggestions found for PR.')
pr_body = "## PR Code Suggestions\n\nNo code suggestions found for PR."
get_logger().debug(f"PR output", artifact=pr_body)
if self.progress_response:
self.git_provider.edit_comment(self.progress_response, body=pr_body)
else:
self.git_provider.publish_comment(pr_body)
return 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):
get_logger().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:
get_logger().info('Pushing PR code suggestions...')
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
if get_settings().pr_code_suggestions.summarize: if get_settings().pr_code_suggestions.summarize and self.git_provider.is_supported("gfm_markdown"):
get_logger().info('Pushing summarize code suggestions...')
self.publish_summarizes_suggestions(data) # generate summarized suggestions
pr_body = self.generate_summarized_suggestions(data)
get_logger().debug(f"PR output", artifact=pr_body)
# add usage guide
if get_settings().pr_code_suggestions.enable_help_text:
pr_body += "<hr>\n\n<details> <summary><strong>✨ Improve tool usage guide:</strong></summary><hr> \n\n"
pr_body += HelpMessage.get_improve_usage_guide()
pr_body += "\n</details>\n"
if self.progress_response:
self.git_provider.edit_comment(self.progress_response, body=pr_body)
else:
self.git_provider.publish_comment(pr_body)
else: else:
get_logger().info('Pushing inline code suggestions...')
self.push_inline_code_suggestions(data) self.push_inline_code_suggestions(data)
if self.progress_response:
self.progress_response.delete()
except Exception as e: except Exception as e:
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}") get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
if self.progress_response:
self.progress_response.delete()
else:
try:
self.git_provider.remove_initial_comment()
self.git_provider.publish_comment(f"Failed to generate code suggestions for PR")
except Exception as e:
pass
async def _prepare_prediction(self, model: str): async def _prepare_prediction(self, model: str):
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)
get_logger().info('Getting AI prediction...') if self.patches_diff:
self.prediction = await self._get_prediction(model) get_logger().debug(f"PR diff", artifact=self.patches_diff)
self.prediction = await self._get_prediction(model, self.patches_diff)
else:
get_logger().error(f"Error getting PR diff")
self.prediction = None
async def _get_prediction(self, model: str): async def _get_prediction(self, model: str, patches_diff: str):
variables = copy.deepcopy(self.vars) variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff variables["diff"] = patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
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:
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, 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
@staticmethod
def _truncate_if_needed(suggestion):
max_code_suggestion_length = get_settings().get("PR_CODE_SUGGESTIONS.MAX_CODE_SUGGESTION_LENGTH", 0)
suggestion_truncation_message = get_settings().get("PR_CODE_SUGGESTIONS.SUGGESTION_TRUNCATION_MESSAGE", "")
if max_code_suggestion_length > 0:
if len(suggestion['improved_code']) > max_code_suggestion_length:
suggestion['improved_code'] = suggestion['improved_code'][:max_code_suggestion_length]
suggestion['improved_code'] += f"\n{suggestion_truncation_message}"
get_logger().info(f"Truncated suggestion from {len(suggestion['improved_code'])} "
f"characters to {max_code_suggestion_length} characters")
return suggestion
def _prepare_pr_code_suggestions(self) -> Dict: def _prepare_pr_code_suggestions(self) -> Dict:
review = self.prediction.strip() review = self.prediction.strip()
data = load_yaml(review) data = load_yaml(review,
keys_fix_yaml=["relevant_file", "suggestion_content", "existing_code", "improved_code"])
if isinstance(data, list): if isinstance(data, list):
data = {'Code suggestions': data} data = {'code_suggestions': data}
# remove invalid suggestions
suggestion_list = []
one_sentence_summary_list = []
for i, suggestion in enumerate(data['code_suggestions']):
if get_settings().pr_code_suggestions.summarize:
if not suggestion or 'one_sentence_summary' not in suggestion or 'label' not in suggestion or 'relevant_file' not in suggestion:
get_logger().debug(f"Skipping suggestion {i + 1}, because it is invalid: {suggestion}")
continue
if suggestion['one_sentence_summary'] in one_sentence_summary_list:
get_logger().debug(f"Skipping suggestion {i + 1}, because it is a duplicate: {suggestion}")
continue
if ('existing_code' in suggestion) and ('improved_code' in suggestion) and (
suggestion['existing_code'] != suggestion['improved_code']):
suggestion = self._truncate_if_needed(suggestion)
if get_settings().pr_code_suggestions.summarize:
one_sentence_summary_list.append(suggestion['one_sentence_summary'])
suggestion_list.append(suggestion)
else:
get_logger().debug(
f"Skipping suggestion {i + 1}, because existing code is equal to improved code {suggestion['existing_code']}")
data['code_suggestions'] = suggestion_list
return data return data
def push_inline_code_suggestions(self, data): def push_inline_code_suggestions(self, data):
code_suggestions = [] code_suggestions = []
if not data['Code suggestions']: if not data['code_suggestions']:
get_logger().info('No suggestions found to improve this PR.') get_logger().info('No suggestions found to improve this PR.')
return self.git_provider.publish_comment('No suggestions found to improve this PR.') if self.progress_response:
return self.git_provider.edit_comment(self.progress_response, body='No suggestions found to improve this PR.')
else:
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: relevant_file = d['relevant_file'].strip()
get_logger().info(f"suggestion: {d}") relevant_lines_start = int(d['relevant_lines_start']) # absolute position
relevant_file = d['relevant file'].strip() relevant_lines_end = int(d['relevant_lines_end'])
relevant_lines_start = int(d['relevant lines start']) # absolute position content = d['suggestion_content'].rstrip()
relevant_lines_end = int(d['relevant lines end']) new_code_snippet = d['improved_code'].rstrip()
content = d['suggestion content'] label = d['label'].strip()
new_code_snippet = d['improved code']
if new_code_snippet: if new_code_snippet:
new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet) new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet)
body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```" body = f"**Suggestion:** {content} [{label}]\n```suggestion\n" + new_code_snippet + "\n```"
code_suggestions.append({'body': body, 'relevant_file': relevant_file, code_suggestions.append({'body': body, 'relevant_file': relevant_file,
'relevant_lines_start': relevant_lines_start, 'relevant_lines_start': relevant_lines_start,
'relevant_lines_end': relevant_lines_end}) 'relevant_lines_end': relevant_lines_end})
except Exception: except Exception:
if get_settings().config.verbosity_level >= 2: get_logger().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:
@ -156,7 +248,8 @@ class PRCodeSuggestions:
original_initial_line = None original_initial_line = None
for file in self.diff_files: for file in self.diff_files:
if file.filename.strip() == relevant_file: if file.filename.strip() == relevant_file:
original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1] if file.head_file: # in bitbucket, head_file is empty. toDo: fix this
original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1]
break break
if original_initial_line: if original_initial_line:
suggested_initial_line = new_code_snippet.splitlines()[0] suggested_initial_line = new_code_snippet.splitlines()[0]
@ -166,34 +259,49 @@ class PRCodeSuggestions:
if delta_spaces > 0: if delta_spaces > 0:
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: get_logger().error(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
def _get_is_extended(self, args: list[str]) -> bool:
"""Check if extended mode should be enabled by the `--extended` flag or automatically according to the configuration"""
if any(["extended" in arg for arg in args]):
get_logger().info("Extended mode is enabled by the `--extended` flag")
return True
if get_settings().pr_code_suggestions.auto_extended_mode:
get_logger().info("Extended mode is enabled automatically based on the configuration toggle")
return True
return False
async def _prepare_prediction_extended(self, model: str) -> dict: async def _prepare_prediction_extended(self, model: str) -> dict:
get_logger().info('Getting PR diff...') self.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) if self.patches_diff_list:
get_logger().debug(f"PR diff", artifact=self.patches_diff_list)
get_logger().info('Getting multi AI predictions...') # parallelize calls to AI:
prediction_list = [] if get_settings().pr_code_suggestions.parallel_calls:
for i, patches_diff in enumerate(patches_diff_list): prediction_list = await asyncio.gather(
get_logger().info(f"Processing chunk {i + 1} of {len(patches_diff_list)}") *[self._get_prediction(model, patches_diff) for patches_diff in self.patches_diff_list])
self.patches_diff = patches_diff self.prediction_list = prediction_list
prediction = await self._get_prediction(model)
prediction_list.append(prediction)
self.prediction_list = prediction_list
data = {}
for prediction in prediction_list:
self.prediction = prediction
data_per_chunk = self._prepare_pr_code_suggestions()
if "Code suggestions" in data:
data["Code suggestions"].extend(data_per_chunk["Code suggestions"])
else: else:
data.update(data_per_chunk) prediction_list = []
self.data = data for i, patches_diff in enumerate(self.patches_diff_list):
prediction = await self._get_prediction(model, patches_diff)
prediction_list.append(prediction)
data = {}
for prediction in prediction_list:
self.prediction = prediction
data_per_chunk = self._prepare_pr_code_suggestions()
if "code_suggestions" in data:
data["code_suggestions"].extend(data_per_chunk["code_suggestions"])
else:
data.update(data_per_chunk)
self.data = data
else:
get_logger().error(f"Error getting PR diff")
self.data = data = None
return data return data
async def rank_suggestions(self, data: List) -> List: async def rank_suggestions(self, data: List) -> List:
@ -208,13 +316,15 @@ class PRCodeSuggestions:
""" """
suggestion_list = [] suggestion_list = []
# remove invalid suggestions if not data:
for i, suggestion in enumerate(data): return suggestion_list
if suggestion['existing code'] != suggestion['improved code']: for suggestion in data:
suggestion_list.append(suggestion) suggestion_list.append(suggestion)
data_sorted = [[]] * len(suggestion_list) data_sorted = [[]] * len(suggestion_list)
if len(suggestion_list) == 1:
return suggestion_list
try: try:
suggestion_str = "" suggestion_str = ""
for i, suggestion in enumerate(suggestion_list): for i, suggestion in enumerate(suggestion_list):
@ -226,9 +336,6 @@ class PRCodeSuggestions:
system_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.system).render( system_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.system).render(
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:
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, system=system_prompt, response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt,
user=user_prompt) user=user_prompt)
@ -239,8 +346,14 @@ class PRCodeSuggestions:
data_sorted[importance_order - 1] = suggestion_list[suggestion_number - 1] data_sorted[importance_order - 1] = suggestion_list[suggestion_number - 1]
if get_settings().pr_code_suggestions.final_clip_factor != 1: if get_settings().pr_code_suggestions.final_clip_factor != 1:
new_len = int(0.5 + len(data_sorted) * get_settings().pr_code_suggestions.final_clip_factor) max_len = max(
data_sorted = data_sorted[:new_len] len(data_sorted),
get_settings().pr_code_suggestions.num_code_suggestions,
get_settings().pr_code_suggestions.num_code_suggestions_per_chunk,
)
new_len = int(0.5 + max_len * get_settings().pr_code_suggestions.final_clip_factor)
if new_len < len(data_sorted):
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:
get_logger().info(f"Could not sort suggestions, error: {e}") get_logger().info(f"Could not sort suggestions, error: {e}")
@ -248,9 +361,13 @@ class PRCodeSuggestions:
return data_sorted return data_sorted
def publish_summarizes_suggestions(self, data: Dict): def generate_summarized_suggestions(self, data: Dict) -> str:
try: try:
data_markdown = "## PR Code Suggestions\n\n" pr_body = "## PR Code Suggestions\n\n"
if len(data.get('code_suggestions', [])) == 0:
pr_body += "No suggestions found to improve this PR."
return pr_body
language_extension_map_org = get_settings().language_extension_map_org language_extension_map_org = get_settings().language_extension_map_org
extension_to_language = {} extension_to_language = {}
@ -258,31 +375,82 @@ class PRCodeSuggestions:
for ext in extensions: for ext in extensions:
extension_to_language[ext] = language extension_to_language[ext] = language
for s in data['Code suggestions']: pr_body = "## PR Code Suggestions\n\n"
try:
extension_s = s['relevant file'].rsplit('.')[-1] pr_body += "<table>"
code_snippet_link = self.git_provider.get_line_link(s['relevant file'], s['relevant lines start'], header = f"Suggestions"
s['relevant lines end']) delta = 76
data_markdown += f"\n💡 Suggestion:\n\n**{s['suggestion content']}**\n\n" header += "&nbsp; " * delta
if code_snippet_link: pr_body += f"""<thead><tr><td>Category</td><td align=left>{header}</td></tr></thead>"""
data_markdown += f" File: [{s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})]({code_snippet_link})\n\n" pr_body += """<tbody>"""
suggestions_labels = dict()
# add all suggestions related to each label
for suggestion in data['code_suggestions']:
label = suggestion['label'].strip().strip("'").strip('"')
if label not in suggestions_labels:
suggestions_labels[label] = []
suggestions_labels[label].append(suggestion)
for label, suggestions in suggestions_labels.items():
num_suggestions=len(suggestions)
# pr_body += f"""<tr><td><strong>{label}</strong></td>"""
pr_body += f"""<tr><td rowspan={num_suggestions}><strong>{label.capitalize()}</strong></td>\n"""
# pr_body += f"""<td>"""
# pr_body += f"""<details><summary>{len(suggestions)} suggestions</summary>"""
# pr_body += f"""<table>"""
for i, suggestion in enumerate(suggestions):
relevant_file = suggestion['relevant_file'].strip()
relevant_lines_start = int(suggestion['relevant_lines_start'])
relevant_lines_end = int(suggestion['relevant_lines_end'])
range_str = ""
if relevant_lines_start == relevant_lines_end:
range_str = f"[{relevant_lines_start}]"
else: else:
data_markdown += f"File: {s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})\n\n" range_str = f"[{relevant_lines_start}-{relevant_lines_end}]"
if self.git_provider.is_supported("gfm_markdown"): code_snippet_link = self.git_provider.get_line_link(relevant_file, relevant_lines_start,
data_markdown += "<details> <summary> Example code:</summary>\n\n" relevant_lines_end)
data_markdown += f"___\n\n" # add html table for each suggestion
language_name = "python"
if extension_s and (extension_s in extension_to_language): suggestion_content = suggestion['suggestion_content'].rstrip().rstrip()
language_name = extension_to_language[extension_s]
data_markdown += f"Existing code:\n```{language_name}\n{s['existing code']}\n```\n" suggestion_content = insert_br_after_x_chars(suggestion_content, 90)
data_markdown += f"Improved code:\n```{language_name}\n{s['improved code']}\n```\n" # pr_body += f"<tr><td><details><summary>{suggestion_content}</summary>"
if self.git_provider.is_supported("gfm_markdown"): existing_code = suggestion['existing_code'].rstrip()+"\n"
data_markdown += "</details>\n" improved_code = suggestion['improved_code'].rstrip()+"\n"
data_markdown += "\n___\n\n"
except Exception as e: diff = difflib.unified_diff(existing_code.split('\n'),
get_logger().error(f"Could not parse suggestion: {s}, error: {e}") improved_code.split('\n'), n=999)
self.git_provider.publish_comment(data_markdown) patch_orig = "\n".join(diff)
patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n')
example_code = ""
example_code += f"```diff\n{patch}\n```\n"
if i==0:
pr_body += f"""<td>\n\n"""
else:
pr_body += f"""<tr><td>\n\n"""
suggestion_summary = suggestion['one_sentence_summary'].strip()
if '`' in suggestion_summary:
suggestion_summary = replace_code_tags(suggestion_summary)
# suggestion_summary = suggestion_summary + max((77-len(suggestion_summary)), 0)*"&nbsp;"
pr_body += f"""\n\n<details><summary>{suggestion_summary}</summary>\n\n___\n\n"""
pr_body += f"""
**{suggestion_content}**
[{relevant_file} {range_str}]({code_snippet_link})
{example_code}
"""
pr_body += f"</details>"
pr_body += f"</td></tr>"
# pr_body += "</details>"
pr_body += """</td></tr>"""
pr_body += """</tr></tbody></table>"""
return pr_body
except Exception as e: except Exception as e:
get_logger().info(f"Failed to publish summarized code suggestions, error: {e}") get_logger().info(f"Failed to publish summarized code suggestions, error: {e}")
return ""

View File

@ -7,7 +7,7 @@ class PRConfig:
""" """
The PRConfig class is responsible for listing all configuration options available for the user. The PRConfig class is responsible for listing all configuration options available for the user.
""" """
def __init__(self, pr_url: str, args=None): def __init__(self, pr_url: str, args=None, ai_handler=None):
""" """
Initialize the PRConfig object with the necessary attributes and objects to comment on a pull request. Initialize the PRConfig object with the necessary attributes and objects to comment on a pull request.

View File

@ -1,21 +1,25 @@
import copy import copy
import re import re
from functools import partial
from typing import List, Tuple from typing import List, Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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, set_custom_labels, get_user_labels from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels, ModelType
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 from pr_agent.log import get_logger
from pr_agent.servers.help import HelpMessage
class PRDescription: class PRDescription:
def __init__(self, pr_url: str, args: list = None): def __init__(self, pr_url: str, args: list = None,
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
""" """
Initialize the PRDescription object with the necessary attributes and objects for generating a PR description Initialize the PRDescription object with the necessary attributes and objects for generating a PR description
using an AI model. using an AI model.
@ -32,11 +36,12 @@ class PRDescription:
if get_settings().pr_description.enable_semantic_files_types and not self.git_provider.is_supported( if get_settings().pr_description.enable_semantic_files_types and not self.git_provider.is_supported(
"gfm_markdown"): "gfm_markdown"):
get_logger().debug(f"Disabling semantic files types for {self.pr_id}") get_logger().debug(f"Disabling semantic files types for {self.pr_id}, gfm_markdown not supported.")
get_settings().pr_description.enable_semantic_files_types = False get_settings().pr_description.enable_semantic_files_types = False
# Initialize the AI handler # Initialize the AI handler
self.ai_handler = AiHandler() self.ai_handler = ai_handler()
# Initialize the variables dictionary # Initialize the variables dictionary
self.vars = { self.vars = {
@ -45,17 +50,14 @@ class PRDescription:
"description": self.git_provider.get_pr_description(full=False), "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, "enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function "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, "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()
# Initialize the token handler # Initialize the token handler
self.token_handler = TokenHandler( self.token_handler = TokenHandler(
self.git_provider.pr, self.git_provider.pr,
@ -63,59 +65,79 @@ class PRDescription:
get_settings().pr_description_prompt.system, get_settings().pr_description_prompt.system,
get_settings().pr_description_prompt.user, get_settings().pr_description_prompt.user,
) )
# Initialize patches_diff and prediction attributes # Initialize patches_diff and prediction attributes
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.file_label_dict = None
self.COLLAPSIBLE_FILE_LIST_THRESHOLD = 8
async def run(self): async def run(self):
"""
Generates a PR description using an AI model and publishes it to the PR.
"""
try: try:
get_logger().info(f"Generating a PR description {self.pr_id}") get_logger().info(f"Generating a PR description for pr_id: {self.pr_id}")
relevant_configs = {'pr_description': dict(get_settings().pr_description),
'config': dict(get_settings().config)}
get_logger().debug("Relevant configs", artifacts=relevant_configs)
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, ModelType.TURBO) # turbo model because larger context
get_logger().info(f"Preparing answer {self.pr_id}")
if self.prediction: if self.prediction:
self._prepare_data() self._prepare_data()
else: else:
get_logger().error(f"Error getting AI prediction {self.pr_id}")
self.git_provider.remove_initial_comment()
return None return None
if get_settings().pr_description.enable_semantic_files_types: if get_settings().pr_description.enable_semantic_files_types:
self._prepare_file_labels() self.file_label_dict = self._prepare_file_labels()
pr_labels = [] pr_labels, pr_file_changes = [], []
if get_settings().pr_description.publish_labels: if get_settings().pr_description.publish_labels:
pr_labels = self._prepare_labels() pr_labels = self._prepare_labels()
if get_settings().pr_description.use_description_markers: if get_settings().pr_description.use_description_markers:
pr_title, pr_body = self._prepare_pr_answer_with_markers() pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer_with_markers()
else: else:
pr_title, pr_body, = self._prepare_pr_answer() pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer()
full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}" if not self.git_provider.is_supported(
"publish_file_comments") or not get_settings().pr_description.inline_file_summary:
pr_body += "\n\n" + changes_walkthrough
get_logger().debug("PR output", artifact={"title": pr_title, "body": pr_body})
# Add help text if gfm_markdown is supported
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_description.enable_help_text:
pr_body += "<hr>\n\n<details> <summary><strong>✨ Describe tool usage guide:</strong></summary><hr> \n\n"
pr_body += HelpMessage.get_describe_usage_guide()
pr_body += "\n</details>\n"
elif get_settings().pr_description.enable_help_comment:
pr_body += "\n\n___\n\n> ✨ **PR-Agent usage**:"
pr_body += "\n>Comment `/help` on the PR to get a list of all available PR-Agent tools and their descriptions\n\n"
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing answer {self.pr_id}") # publish labels
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
original_labels = self.git_provider.get_pr_labels()
get_logger().debug(f"original labels", artifact=original_labels)
user_labels = get_user_labels(original_labels)
get_logger().debug(f"published labels:\n{pr_labels + user_labels}")
self.git_provider.publish_labels(pr_labels + user_labels)
# publish description
if get_settings().pr_description.publish_description_as_comment: if get_settings().pr_description.publish_description_as_comment:
full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}"
self.git_provider.publish_comment(full_markdown_description) self.git_provider.publish_comment(full_markdown_description)
else: else:
self.git_provider.publish_description(pr_title, pr_body) self.git_provider.publish_description(pr_title, pr_body)
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
current_labels = self.git_provider.get_labels()
user_labels = get_user_labels(current_labels)
self.git_provider.publish_labels(pr_labels + user_labels)
if (get_settings().pr_description.final_update_message and # publish final update message
hasattr(self.git_provider, 'pr_url') and self.git_provider.pr_url): if (get_settings().pr_description.final_update_message):
latest_commit_url = self.git_provider.get_latest_commit_url() latest_commit_url = self.git_provider.get_latest_commit_url()
if latest_commit_url: if latest_commit_url:
self.git_provider.publish_comment( pr_url = self.git_provider.get_pr_url()
f"**[PR Description]({self.git_provider.pr_url})** updated to latest commit ({latest_commit_url})") update_comment = f"**[PR Description]({pr_url})** updated to latest commit ({latest_commit_url})"
self.git_provider.publish_comment(update_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
except Exception as e: except Exception as e:
get_logger().error(f"Error generating PR description {self.pr_id}: {e}") get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
@ -123,26 +145,16 @@ class PRDescription:
return "" return ""
async def _prepare_prediction(self, model: str) -> None: async def _prepare_prediction(self, model: str) -> None:
"""
Prepare the AI prediction for the PR description 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.
"""
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description: if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
return None 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)
get_logger().info(f"Getting AI prediction {self.pr_id}") if self.patches_diff:
self.prediction = await self._get_prediction(model) get_logger().debug(f"PR diff", artifact=self.patches_diff)
self.prediction = await self._get_prediction(model)
else:
get_logger().error(f"Error getting PR diff {self.pr_id}")
self.prediction = None
async def _get_prediction(self, model: str) -> str: async def _get_prediction(self, model: str) -> str:
""" """
@ -158,14 +170,11 @@ class PRDescription:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables) set_custom_labels(variables, self.git_provider)
self.variables = 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:
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( response, finish_reason = await self.ai_handler.chat_completion(
model=model, model=model,
temperature=0.2, temperature=0.2,
@ -173,9 +182,6 @@ 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_data(self): def _prepare_data(self):
@ -185,6 +191,22 @@ class PRDescription:
if get_settings().pr_description.add_original_user_description and self.user_description: if get_settings().pr_description.add_original_user_description and self.user_description:
self.data["User Description"] = self.user_description self.data["User Description"] = self.user_description
# re-order keys
if 'User Description' in self.data:
self.data['User Description'] = self.data.pop('User Description')
if 'title' in self.data:
self.data['title'] = self.data.pop('title')
if 'type' in self.data:
self.data['type'] = self.data.pop('type')
if 'labels' in self.data:
self.data['labels'] = self.data.pop('labels')
if 'description' in self.data:
self.data['description'] = self.data.pop('description')
if 'pr_files' in self.data:
self.data['pr_files'] = self.data.pop('pr_files')
def _prepare_labels(self) -> List[str]: def _prepare_labels(self) -> List[str]:
pr_types = [] pr_types = []
@ -200,9 +222,19 @@ class PRDescription:
pr_types = self.data['type'] pr_types = self.data['type']
elif type(self.data['type']) == str: elif type(self.data['type']) == str:
pr_types = self.data['type'].split(',') pr_types = self.data['type'].split(',')
# convert lowercase labels to original case
try:
if "labels_minimal_to_labels_dict" in self.variables:
d: dict = self.variables["labels_minimal_to_labels_dict"]
for i, label_i in enumerate(pr_types):
if label_i in d:
pr_types[i] = d[label_i]
except Exception as e:
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
return pr_types return pr_types
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]: def _prepare_pr_answer_with_markers(self) -> Tuple[str, str, str, List[dict]]:
get_logger().info(f"Using description marker replacements {self.pr_id}") get_logger().info(f"Using description marker replacements {self.pr_id}")
title = self.vars["title"] title = self.vars["title"]
body = self.user_description body = self.user_description
@ -221,20 +253,21 @@ class PRDescription:
summary = f"{ai_header}{ai_summary}" summary = f"{ai_header}{ai_summary}"
body = body.replace('pr_agent:summary', summary) body = body.replace('pr_agent:summary', summary)
if not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body): ai_walkthrough = self.data.get('pr_files')
ai_walkthrough = self.data.get('PR Main Files Walkthrough') walkthrough_gfm = ""
if ai_walkthrough: pr_file_changes = []
walkthrough = str(ai_header) if ai_walkthrough and not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
for file in ai_walkthrough: try:
filename = file['filename'].replace("'", "`") walkthrough_gfm, pr_file_changes = self.process_pr_files_prediction(walkthrough_gfm,
description = file['changes in file'].replace("'", "`") self.file_label_dict)
walkthrough += f'- `{filename}`: {description}\n' body = body.replace('pr_agent:walkthrough', walkthrough_gfm)
except Exception as e:
get_logger().error(f"Failing to process walkthrough {self.pr_id}: {e}")
body = body.replace('pr_agent:walkthrough', "")
body = body.replace('pr_agent:walkthrough', walkthrough) return title, body, walkthrough_gfm, pr_file_changes
return title, body def _prepare_pr_answer(self) -> Tuple[str, str, str, List[dict]]:
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.
@ -251,7 +284,7 @@ class PRDescription:
if not get_settings().pr_description.enable_pr_type: if not get_settings().pr_description.enable_pr_type:
self.data.pop('type') self.data.pop('type')
for key, value in self.data.items(): for key, value in self.data.items():
markdown_text += f"## {key}\n\n" markdown_text += f"## **{key}**\n\n"
markdown_text += f"{value}\n\n" markdown_text += f"{value}\n\n"
# Remove the 'PR Title' key from the dictionary # Remove the 'PR Title' key from the dictionary
@ -265,14 +298,14 @@ 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, changes_walkthrough = "", ""
pr_file_changes = []
for idx, (key, value) in enumerate(self.data.items()): for idx, (key, value) in enumerate(self.data.items()):
if key == 'pr_files': if key == 'pr_files':
value = self.file_label_dict value = self.file_label_dict
key_publish = "PR changes walkthrough"
else: else:
key_publish = key.rstrip(':').replace("_", " ").capitalize() key_publish = key.rstrip(':').replace("_", " ").capitalize()
pr_body += f"## {key_publish}\n" pr_body += f"## **{key_publish}**\n"
if 'walkthrough' in key.lower(): if 'walkthrough' in key.lower():
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
pr_body += "<details> <summary>files:</summary>\n\n" pr_body += "<details> <summary>files:</summary>\n\n"
@ -283,64 +316,80 @@ class PRDescription:
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
pr_body += "</details>\n" pr_body += "</details>\n"
elif 'pr_files' in key.lower(): elif 'pr_files' in key.lower():
pr_body = self.process_pr_files_prediction(pr_body, value) changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
changes_walkthrough = f"## **Changes walkthrough**\n{changes_walkthrough}"
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 isinstance(value, list): if isinstance(value, list):
value = ', '.join(v for v in value) value = ', '.join(v for v in value)
pr_body += f"{value}\n" pr_body += f"{value}\n"
if idx < len(self.data) - 1: if idx < len(self.data) - 1:
pr_body += "\n___\n" pr_body += "\n\n___\n\n"
if get_settings().config.verbosity_level >= 2: return title, pr_body, changes_walkthrough, pr_file_changes,
get_logger().info(f"title:\n{title}\n{pr_body}")
return title, pr_body
def _prepare_file_labels(self): def _prepare_file_labels(self):
self.file_label_dict = {} file_label_dict = {}
for file in self.data['pr_files']: for file in self.data['pr_files']:
try: try:
filename = file['filename'].replace("'", "`").replace('"', '`') filename = file['filename'].replace("'", "`").replace('"', '`')
changes_summary = file['changes_summary'] changes_summary = file['changes_summary']
label = file['label'] changes_title = file['changes_title'].strip()
if label not in self.file_label_dict: label = file.get('label')
self.file_label_dict[label] = [] if label not in file_label_dict:
self.file_label_dict[label].append((filename, changes_summary)) file_label_dict[label] = []
file_label_dict[label].append((filename, changes_title, changes_summary))
except Exception as e: except Exception as e:
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}") get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
pass pass
return file_label_dict
def process_pr_files_prediction(self, pr_body, value): def process_pr_files_prediction(self, pr_body, value):
if not self.git_provider.is_supported("gfm_markdown"): pr_comments = []
get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported") # logic for using collapsible file list
return pr_body use_collapsible_file_list = get_settings().pr_description.collapsible_file_list
num_files = 0
if value:
for semantic_label in value.keys():
num_files += len(value[semantic_label])
if use_collapsible_file_list == "adaptive":
use_collapsible_file_list = num_files > self.COLLAPSIBLE_FILE_LIST_THRESHOLD
if not self.git_provider.is_supported("gfm_markdown"):
return pr_body
try: try:
pr_body += "<table>" pr_body += "<table>"
header = f"Relevant files" header = f"Relevant files"
delta = 65 delta = 75
header += "&nbsp; " * delta # header += "&nbsp; " * delta
pr_body += f"""<thead><tr><th></th><th>{header}</th></tr></thead>""" pr_body += f"""<thead><tr><th></th><th align="left">{header}</th></tr></thead>"""
pr_body += """<tbody>""" pr_body += """<tbody>"""
for semantic_label in value.keys(): for semantic_label in value.keys():
s_label = semantic_label.strip("'").strip('"') s_label = semantic_label.strip("'").strip('"')
pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>""" pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>"""
list_tuples = value[semantic_label] 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: if use_collapsible_file_list:
filename = filename.replace("'", "`") pr_body += f"""<td><details><summary>{len(list_tuples)} files</summary><table>"""
else:
pr_body += f"""<td><table>"""
for filename, file_changes_title, file_change_description in list_tuples:
filename = filename.replace("'", "`").rstrip()
filename_publish = filename.split("/")[-1] filename_publish = filename.split("/")[-1]
filename_publish = f"{filename_publish}" file_changes_title_code = f"<code>{file_changes_title}</code>"
if len(filename_publish) < (delta - 5): file_changes_title_code_br = insert_br_after_x_chars(file_changes_title_code, x=(delta - 5)).strip()
filename_publish += "&nbsp; " * ((delta - 5) - len(filename_publish)) if len(file_changes_title_code_br) < (delta - 5):
file_changes_title_code_br += "&nbsp; " * ((delta - 5) - len(file_changes_title_code_br))
filename_publish = f"<strong>{filename_publish}</strong><dd>{file_changes_title_code_br}</dd>"
diff_plus_minus = "" diff_plus_minus = ""
delta_nbsp = ""
diff_files = self.git_provider.diff_files diff_files = self.git_provider.diff_files
for f in diff_files: for f in diff_files:
if f.filename.lower() == filename.lower(): if f.filename.lower() == filename.lower():
num_plus_lines = f.num_plus_lines num_plus_lines = f.num_plus_lines
num_minus_lines = f.num_minus_lines num_minus_lines = f.num_minus_lines
diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}" diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}"
delta_nbsp = "&nbsp; " * max(0, (8 - len(diff_plus_minus)))
break break
# try to add line numbers link to code suggestions # try to add line numbers link to code suggestions
@ -349,49 +398,105 @@ class PRDescription:
filename = filename.strip() filename = filename.strip()
link = self.git_provider.get_line_link(filename, relevant_line_start=-1) 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)) file_change_description_br = insert_br_after_x_chars(file_change_description, x=(delta - 5))
pr_body += f""" pr_body += f"""
<tr> <tr>
<td> <td>
<details> <details>
<summary><strong>{filename_publish}</strong></summary> <summary>{filename_publish}</summary>
<ul> <hr>
{filename}<br><br>
<strong>{file_change_description}</strong>
</ul>
</details>
</td>
<td><a href="{link}"> {diff_plus_minus}</a></td>
{filename}
{file_change_description_br}
</details>
</td>
<td><a href="{link}">{diff_plus_minus}</a>{delta_nbsp}</td>
</tr> </tr>
""" """
pr_body += """</table></details></td></tr>""" if use_collapsible_file_list:
pr_body += """</table></details></td></tr>"""
else:
pr_body += """</table></td></tr>"""
pr_body += """</tr></tbody></table>""" pr_body += """</tr></tbody></table>"""
except Exception as e: except Exception as e:
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {e}") get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {e}")
pass pass
return pr_body return pr_body, pr_comments
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(' ') def count_chars_without_html(string):
new_text = "" if '<' not in string:
current_length = 0 return len(string)
no_html_string = re.sub('<[^>]+>', '', string)
return len(no_html_string)
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 def insert_br_after_x_chars(text, x=70):
new_text += word + " " """
current_length += len(word) + 1 # Add 1 for the space Insert <br> into a string after a word that increases its length above x characters.
Use proper HTML tags for code and new lines.
"""
if count_chars_without_html(text) < x:
return text
return new_text.strip() # Remove trailing space # replace odd instances of ` with <code> and even instances of ` with </code>
text = replace_code_tags(text)
# convert list items to <li>
if text.startswith("- "):
text = "<li>" + text[2:]
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
# convert new lines to <br>
text = text.replace("\n", '<br>')
# split text into lines
lines = text.split('<br>')
words = []
for i, line in enumerate(lines):
words += line.split(' ')
if i < len(lines) - 1:
words[-1] += "<br>"
new_text = []
is_inside_code = False
current_length = 0
for word in words:
is_saved_word = False
if word == "<code>" or word == "</code>" or word == "<li>" or word == "<br>":
is_saved_word = True
len_word = count_chars_without_html(word)
if not is_saved_word and (current_length + len_word > x):
if is_inside_code:
new_text.append("</code><br><code>")
else:
new_text.append("<br>")
current_length = 0 # Reset counter
new_text.append(word + " ")
if not is_saved_word:
current_length += len_word + 1 # Add 1 for the space
if word == "<li>" or word == "<br>":
current_length = 0
if "<code>" in word:
is_inside_code = True
if "</code>" in word:
is_inside_code = False
return ''.join(new_text).strip()
def replace_code_tags(text):
"""
Replace odd instances of ` with <code> and even instances of ` with </code>
"""
parts = text.split('`')
for i in range(1, len(parts), 2):
parts[i] = '<code>' + parts[i] + '</code>'
return ''.join(parts)

View File

@ -1,10 +1,12 @@
import copy import copy
import re import re
from functools import partial
from typing import List, Tuple from typing import List, Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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, set_custom_labels, get_user_labels from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
@ -15,7 +17,8 @@ from pr_agent.log import get_logger
class PRGenerateLabels: class PRGenerateLabels:
def __init__(self, pr_url: str, args: list = None): def __init__(self, pr_url: str, args: list = None,
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
""" """
Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels
corresponding to the PR using an AI model. corresponding to the PR using an AI model.
@ -31,7 +34,7 @@ class PRGenerateLabels:
self.pr_id = self.git_provider.get_pr_id() self.pr_id = self.git_provider.get_pr_id()
# Initialize the AI handler # Initialize the AI handler
self.ai_handler = AiHandler() self.ai_handler = ai_handler()
# Initialize the variables dictionary # Initialize the variables dictionary
self.vars = { self.vars = {
@ -40,7 +43,6 @@ class PRGenerateLabels:
"description": self.git_provider.get_pr_description(full=False), "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, "enable_custom_labels": get_settings().config.enable_custom_labels,
@ -82,7 +84,7 @@ class PRGenerateLabels:
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing labels {self.pr_id}") get_logger().info(f"Pushing labels {self.pr_id}")
current_labels = self.git_provider.get_labels() current_labels = self.git_provider.get_pr_labels()
user_labels = get_user_labels(current_labels) user_labels = get_user_labels(current_labels)
pr_labels = pr_labels + user_labels pr_labels = pr_labels + user_labels
@ -132,14 +134,11 @@ class PRGenerateLabels:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables) set_custom_labels(variables, self.git_provider)
self.variables = variables
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables)
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( response, finish_reason = await self.ai_handler.chat_completion(
model=model, model=model,
temperature=0.2, temperature=0.2,
@ -147,9 +146,6 @@ class PRGenerateLabels:
user=user_prompt user=user_prompt
) )
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"\nAI response:\n{response}")
return response return response
def _prepare_data(self): def _prepare_data(self):
@ -168,4 +164,14 @@ class PRGenerateLabels:
elif type(self.data['labels']) == str: elif type(self.data['labels']) == str:
pr_types = self.data['labels'].split(',') pr_types = self.data['labels'].split(',')
# convert lowercase labels to original case
try:
if "labels_minimal_to_labels_dict" in self.variables:
d: dict = self.variables["labels_minimal_to_labels_dict"]
for i, label_i in enumerate(pr_types):
if label_i in d:
pr_types[i] = d[label_i]
except Exception as e:
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
return pr_types return pr_types

View File

@ -0,0 +1,99 @@
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider, GithubProvider
from pr_agent.log import get_logger
class PRHelpMessage:
def __init__(self, pr_url: str, args=None, ai_handler=None):
self.git_provider = get_git_provider()(pr_url)
async def run(self):
try:
if not self.git_provider.is_supported("gfm_markdown"):
self.git_provider.publish_comment(
"The `Help` tool requires gfm markdown, which is not supported by your code platform.")
return
get_logger().info('Getting PR Help Message...')
relevant_configs = {'pr_help': dict(get_settings().pr_help),
'config': dict(get_settings().config)}
get_logger().debug("Relevant configs", artifacts=relevant_configs)
pr_comment = "## PR Agent Walkthrough\n\n"
pr_comment += "🤖 Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more."""
pr_comment += "\n\nHere is a list of tools you can use to interact with the PR Agent:\n"
base_path = "https://github.com/Codium-ai/pr-agent/tree/main/docs"
tool_names = []
tool_names.append(f"[DESCRIBE]({base_path}/DESCRIBE.md)")
tool_names.append(f"[REVIEW]({base_path}/REVIEW.md)")
tool_names.append(f"[IMPROVE]({base_path}/IMPROVE.md)")
tool_names.append(f"[ANALYZE]({base_path}/Analyze.md) 💎")
tool_names.append(f"[UPDATE CHANGELOG]({base_path}/UPDATE_CHANGELOG.md)")
tool_names.append(f"[ADD DOCUMENTATION]({base_path}/ADD_DOCUMENTATION.md) 💎")
tool_names.append(f"[ASK]({base_path}/ASK.md)")
tool_names.append(f"[GENERATE CUSTOM LABELS]({base_path}/GENERATE_CUSTOM_LABELS.md)")
tool_names.append(f"[TEST]({base_path}/TEST.md) 💎")
tool_names.append(f"[CI FEEDBACK]({base_path}/CI_FEEDBACK.md) 💎")
tool_names.append(f"[CUSTOM SUGGESTIONS]({base_path}/CUSTOM_SUGGESTIONS.md) 💎")
tool_names.append(f"[SIMILAR ISSUE]({base_path}/SIMILAR_ISSUE.md)")
descriptions = []
descriptions.append("Generates PR description - title, type, summary, code walkthrough and labels")
descriptions.append("Adjustable feedback about the PR, possible issues, security concerns, review effort and more")
descriptions.append("Code suggestions for improving the PR.")
descriptions.append("Identifies code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component.")
descriptions.append("Automatically updates the changelog.")
descriptions.append("Generates documentation to methods/functions/classes that changed in the PR.")
descriptions.append("Answering free-text questions about the PR.")
descriptions.append("Generates custom labels for the PR, based on specific guidelines defined by the user")
descriptions.append("Generates unit tests for a specific component, based on the PR code change.")
descriptions.append("Generates feedback and analysis for a failed CI job.")
descriptions.append("Generates custom suggestions for improving the PR code, based on specific guidelines defined by the user.")
descriptions.append("Automatically retrieves and presents similar issues.")
commands =[]
commands.append("`/describe`")
commands.append("`/review`")
commands.append("`/improve`")
commands.append("`/analyze`")
commands.append("`/update_changelog`")
commands.append("`/add_docs`")
commands.append("`/ask`")
commands.append("`/generate_labels`")
commands.append("`/test`")
commands.append("`/checks`")
commands.append("`/custom_suggestions`")
commands.append("`/similar_issue`")
checkbox_list = []
checkbox_list.append(" - [ ] Run <!-- /describe -->")
checkbox_list.append(" - [ ] Run <!-- /review -->")
checkbox_list.append(" - [ ] Run <!-- /improve -->")
checkbox_list.append(" - [ ] Run <!-- /analyze -->")
checkbox_list.append(" - [ ] Run <!-- /update_changelog -->")
checkbox_list.append(" - [ ] Run <!-- /add_docs -->")
checkbox_list.append("[*]")
checkbox_list.append("[*]")
checkbox_list.append("[*]")
checkbox_list.append("[*]")
checkbox_list.append("[*]")
checkbox_list.append("[*]")
if isinstance(self.git_provider, GithubProvider):
pr_comment += f"<table><tr align='center'><th align='center'>Tool</th><th align='center'>Description</th><th align='center'>Invoke Interactively :gem:</th></tr>"
for i in range(len(tool_names)):
pr_comment += f"\n<tr><td align='center'>\n\n<strong>{tool_names[i]}</strong></td>\n<td>{descriptions[i]}</td>\n<td>\n\n{checkbox_list[i]}\n</td></tr>"
pr_comment += "</table>\n\n"
pr_comment += f"""\n\n(1) Note that each tool be [triggered automatically](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools-for-pr-actions) when a new PR is opened, or called manually by [commenting on a PR](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#online-usage)."""
pr_comment += f"""\n\n(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the `/ask` tool, you need to comment on a PR: `/ask "<question content>"`. See the relevant documentation for each tool for more details."""
else:
pr_comment += f"<table><tr align='center'><th align='center'>Tool</th><th align='left'>Command</th><th align='left'>Description</th></tr>"
for i in range(len(tool_names)):
pr_comment += f"\n<tr><td align='center'>\n\n<strong>{tool_names[i]}</strong></td><td>{commands[i]}</td><td>{descriptions[i]}</td></tr>"
pr_comment += "</table>\n\n"
pr_comment += f"""\n\nNote that each tool be [invoked automatically](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools-for-pr-actions) when a new PR is opened, or called manually by [commenting on a PR](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#online-usage)."""
if get_settings().config.publish_output:
self.git_provider.publish_comment(pr_comment)
except Exception as e:
get_logger().error(f"Error while running PRHelpMessage: {e}")
return ""

Some files were not shown because too many files have changed in this diff Show More